TECH PLAY

電通総研

電通総研 の技術ブログ

834

こんにちは、 電通国際情報サービス (ISID) 金融ソリューション事業部の大場です。 今回は、Rustでフロントエンドの実装ができるYewというライブラリを使って Markdown エディタを作った話をします。本記事は、Yewの内部実装に触れながらYewやRustのマクロの動作について理解を深めることを目的としています。これらについて詳しく知りたい方はぜひ本記事を参考にしていただければと思います。 また、本記事で紹介するコードはこちらの リポジトリ で公開しています。 https://github.com/ISID/wasm-md-editor 作った背景 採用した主要なCrate 全体像とフロー Yew そもそもWebAssembly(Wasm)とは Yew内部で使われる主要なCrate Yewの実装 手続き型マクロ マクロについての補足 #[function_component]実装 実際に画面を実装してみる Top画面とルーティング実装 エディタ画面の作成 最後に 補足 Trunkについて Markdownのスタイルについて 作った背景 WebAssemblyを触ってみたいと思っていたところ、Rustでフロントエンドの実装ができるライブラリを発見したのを機に「これは何か作ってみるしかない!」と一念発起して作り始めました。デザインに時間をかけたくなかったので、最もシンプルなUIで作れそうな Markdown を作ることにしました。( 逆にシンプル過ぎたかもしれません ) 採用した主要なCrate さっそく、 Markdown エディタに使用した主要なライブラリの簡単な紹介です。pulldown-cmarkの実装部分の説明は省いています。気になる方はgit リポジトリ をご覧ください。 Yew WebAssemblyによるRust製フロントエンド フレームワーク JSX記法を提供するHTMLマクロや、ReactライクなComponent(Class Component、Functional Component)の実装が可能 pulldown-cmark Markdown 記法のテキストをHTML形式に変換するParser 全体像とフロー 作ったアプリの全体像がこんな感じです。 ①ユーザーが画面に Markdown 記法でテキストを入力 ➁Yew コンポーネント は文字入力のイベントを受信し、pulldown-cmarkで作られたrendererへテキストを渡す ③rendererは Markdown 記法を解釈し、HTML形式のテキストに変換する ④Yew コンポーネント に返却し再 レンダリング ここからYewの紹介をしつつ、実際にYewの実装を見ていきます。 Yew そもそもWebAssembly(Wasm)とは ブラウザ上で動作するバイナリー形式の アセンブリ言語 で、ネイティブアプリに近いパフォーマンスで動作する言語と言われています。現在Wasmに コンパイル できる言語にはC、 C++ 、Rustがあります。 Wasmには「 JavaScript を補完し、並行して動作するための言語」という思想が根底にあり、 JavaScript と(今回の記事では)Rustが双方向にそれぞれの関数をexport, importして利用することができます。 Yew内部で使われる主要なCrate js-sys JavaScript 標準の ビルトインオブジェクト をRustに提供 web-sys ブラウザが提供するWeb API をRustに提供 wasm-bindgen Rustで書いたコード(関数)を JavaScript 側で利用するためのCrate。両者の関数を受け渡しするブリッジ的な役割を果たす。js-sysやweb-sysを使ったRustのコードもwasm-bindgenが最終的に javaScript にexportする wasm-bindgen-futures Rustと JavaScript 両者の非同期実装をブリッジするためのCrate。 JavaScript のPromiseをRustのFutureとして操作することが可能 Yewの実装 手続き型マクロ 簡易的なComponentを実装してみます。 use yew::prelude::*; // これはHomeコンポーネントとして認識される #[function_component(Home)] // 1. pub fn home() -> Html { html! { // 2. <h1>{"Welcome to my editor!"}</h1> } } #[function_component] アトリビュート を付与することで関数全体がComponentであるとYewが認識します。 アトリビュート の引数にある `home はComponentの名称を指しています。 home関数では、 html! を利用してインナーブロックで与えられたHTMLタグを処理しHTMLとして返却します。 マクロについての補足 1.2 で述べた記法は、Rustの世界ではどちらもマクロと呼ばれています。 1 のように関数や構造体に付与するものは手続き型マクロと呼ばれるのに対し、macro_rules!で定義され、呼び出し元からは関数呼び出しのように利用されるものは宣言的マクロ(println!など)に分類されます。 ここで注意ですが、 2 の html! は一見宣言的マクロに見えますが実は手続き型マクロです。これはhtml!マクロの内部の実装を見ると明らかになります。 #[proc_macro_error::proc_macro_error] #[proc_macro] // 1. pub fn html(input: TokenStream) -> TokenStream { let root = parse_macro_input!(input as HtmlRootVNode); TokenStream::from(root.into_token_stream()) } html!マクロの実装は #[proc_macro] によって定義されており、macro_rules!を使っていません。 #[proc_macro] を使った手続き型マクロは関数風マクロと呼ばれています。手続き型マクロがRustのバージョン1.15.0で追加されたため、従来から存在した宣言的マクロと後発の関数風マクロが共存してしまっているようです。 それでは、 #[function_component] マクロはどのように実装されているのか見ていきましょう。 #[function_component]実装 先に実装から。 #[proc_macro_attribute] // 1. pub fn function_component( attr: proc_macro::TokenStream, // 2. item: proc_macro::TokenStream, // 2. ) -> proc_macro::TokenStream { let item = parse_macro_input!(item as FunctionComponent); // 3. let attr = parse_macro_input!(attr as FunctionComponentName); // 3. function_component_impl(attr, item) // 4. .unwrap_or_else(|err| err.to_compile_error()) .into() } pub fn function_component_impl( name: FunctionComponentName, component: FunctionComponent, ) -> syn::Result<TokenStream> { let FunctionComponentName { component_name } = name; // ・・・省略・・・ let quoted = quote! { // 5. #[doc(hidden)] #[allow(non_camel_case_types)] #[allow(unused_parens)] #vis struct #function_name #impl_generics { _marker: ::std::marker::PhantomData<(#phantom_generics)>, } impl #impl_generics ::yew::functional::FunctionProvider for #function_name #ty_generics #where_clause { type TProps = #props_type; fn run(#arg) -> #ret_type { #block } } #(#attrs)* #[allow(type_alias_bounds)] #vis type #component_name #impl_generics = ::yew::functional::FunctionComponent<#function_name #ty_generics>; }; Ok(quoted) } コンポーネント を定義する際に利用した #[function_component] アトリビュート は下記のように動作します。 proc_macro_attribute はfunction_component()関数が Custom Attribute であることを示しており、利用側で #[function_component] を関数に付与した際には、定義元であるこの関数にリンクされます。 手続き型マクロを定義する関数は、TokenStreamを入力として受け取りTokenStreamを出力として返します。つまり、 アトリビュート を付与した関数のコードそのものが入力値としてTokenStreamに変換され、そのTokenStreamを基にマクロで生成されるコードがTokenStreamとして返却されます。 引数の1つ目である attr: proc_macro::TokenStream は呼び出し側( #[function_component(Home)] )のHomeを指しているのに対し、2つ目の item: proc_macro::TokenStream は #[function_component(Home)] を付与した関数の中身(Componentの実装)に対応しています。 itemがFunctionComponent型、attrはFunctionComponentName型にキャストされていることがわかります。 parse_macro_input!はTokenStreamの トーク ン列を 構文木 にパースし、Rustが解釈できるデータ構造に変換されます。その後、マクロ実装によってデータ構造に対して書き込みが行われていきます。書き込み後に生成されたコードをTokenStreamに再変換し、呼び出し元に返却します。 一般的な手続き型マクロの順番は上述の通りですが、 parse_macro_input! マクロによって 構文木 にパースされたTokenStreamはこのあとfunction_component_impl()関数内の処理で再度TokenStreamに変換されます。 構文木 にパースされると、マクロによってコードが生成されます。function_component_impl()関数を見ると quote! マクロが呼ばれ、 構文木 からTokenStreamへの変換が行われます。 コードを生成する処理については各手続き型マクロの固有処理になるので、マクロを定義するlib.rsからは分離して定義することが多いです。 これは、proc_macroで定義したマクロ内部のコードは、マクロが評価される実行時のタイミングでしか呼び出すことができません。つまり、マクロ内部の詳細な実装をテストすることも踏まえると、詳細な実装はlib.rsではない別のクレートに実装し、lib.rsから呼び出すように書く必要があるのです。 実際に画面を実装してみる Top画面とルーティング実装 Rustのマクロの説明で利用したHome コンポーネント をラップして、トップ画面を作成します。 use crate::{components::home::Home, Routing}; use stylist::style; use yew::prelude::*; use yew_router::{history::History, hooks::use_history}; #[function_component(Top)] // 1. pub fn top() -> Html { let container = style!( // 2. r#" display: flex; flex-direction: column; align-items: center; "# ) .expect("Failed to styled."); let button = style!( r#" color: #ffffff; width: 200px; padding: 10px; background-color: #1976d2; box-shadow: 0 3px 5px rgba(0, 0, 0, .3); -webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, .3); :hover { background: #115293; margin-top: 3px; } "# ) .expect("Failed to styled."); let history = use_history().unwrap(); let onclick = Callback::once(move |_| history.push(Routing::Editor)); html! { <> <div class={container}> <Home /> <button class={button} {onclick}>{"Start"}</ button> </div> </> } Top コンポーネント は、Home コンポーネント を呼び出しており、Startボタンを配置するTopページを表している。 styleは stylist を利用しており、Reactのような宣言的なスタイルの定義が可能。 あとはmain.rsとしてルーティングの設定を行います。今回はHome画面をトップ画面に、Home画面に設置しているボタンを押下するとエディタ画面に遷移するようにします。(解説は省きます) #[derive(Clone, Routable, PartialEq)] pub enum Routing { #[at("/")] Home, #[at("/editor")] Editor, #[not_found] #[at("/404")] NotFound, } pub enum Msg { SetInput(String), } #[function_component(App)] fn app() -> Html { html! { <BrowserRouter> <Switch<Routing> render={Switch::render(switch)} /> </BrowserRouter> } } fn switch(routes: &Routing) -> Html { match routes { Routing::Home => html! { <Top /> }, Routing::Editor => html! { <Text /> }, Routing::NotFound => html! {<NotFound />}, } } fn main() { yew::start_app::<App>(); } サーバを起動すると下記のようなTop画面が表示されます。 エディタ画面の作成 エディタ画面の実装です。改めてですが、画面のデザインは驚くほど凝っていません。 Reactを書いてるような感覚でstyleや イベントハンドラ を実装できるのがわかるかと思います。 #[styled_component(Text)] pub fn text() -> Html { let style = style!( r#" background-color: #1e2126; color: #fff; font-family: inherit; margin: 2rem; "# ) .expect("Failed to styled."); let container = style!( r#" display: flex; "# ) .expect("Failed to styled."); let item = style!( r#" "# ) .expect("Failed to styled."); let value = use_state(|| String::from("")); let on_input = { // 1. let value = value.clone(); Callback::from(move |e: InputEvent| { // 2. let input: HtmlTextAreaElement = e.target_unchecked_into(); value.set(input.value()); }) }; let html = cmark(value.to_string()); // 3. let div = web_sys::window() .unwrap() .document() .unwrap() .create_element("div") .unwrap(); div.set_inner_html(&html); let node = Node::from(div); let vnode = VNode::VRef(node); // 5. html! { <> <div class="markdown-body"> <div class={container}> <div class={item}> <textarea class={style} rows="140" cols="100" value={value.to_string()} oninput={on_input} /> </div> <div class="item" > {vnode} </div> </div> </div> </> } } fn cmark(text: String) -> String { let mut options = Options::empty(); options.insert( Options::ENABLE_TABLES // 4. | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS | Options::ENABLE_SMART_PUNCTUATION, ); let parser = Parser::new_ext(&text, options); let mut html_output = String::new(); // parser changes rendered String for markdown html::push_html(&mut html_output, parser); html_output } テキストエリアに文字が入力された際に発火されるイベントを登録しておきます。 Callback を使ってComponentが保持する状態にテキストの値を渡します。 pulldown-cmarkの処理です。 Markdown 記法で書かれたテキストをHTMLに変換します。 Parserを作成する際に必要なオプションの有無を設定しています。 ここで述べるオプションというのは Markdown 記法のうち、表形式やタスクリストなどの記法をParserの変換対象にするかを指しています。 各Options内の変数は1をシフト演算したものであるため、上記の実装のように 論理和 をとることでまとめてオプションを有効化しています。 HTMLに変換されたテキストを VNode を使って仮想DOMにセットします。 エディタ画面はこんな感じになりました。 最後に 本記事では、Yewの実装を見ながらマクロの挙動やYewの実装方法について説明しました。WasmやRustに興味を持っていただけたら嬉しいです。 また、Rustでフロントエンド実装ができるライブラリはYew以外にもいくつかあるので興味のある方はぜひ見てみてください! 補足 Trunkについて ローカルでのWebサーバは Trunk を利用しました。TrunkはRustのコードを JavaScript モジュールに コンパイル するバンドルツールとしてwasm-packと大変似ていますが、Trunkの場合はwasm-packと違って JavaScript モジュールのほかその他のアセット(HTML, CSS , Image)も同時に自動生成します。また、TrunkにはビルトインでWebサーバが提供されているため、コマンド一発で開発時に利用するWebサーバを立ち上げることができます。 Yew公式でも、Trunkが推奨されています。 Markdown のスタイルについて 公開されているものがあったのでそのまま利用しました。 https://github.com/sindresorhus/github-markdown-css 執筆: 大場 進太郎 (@ShintaroOba) 、レビュー: @sato.taichi ( Shodo で執筆されました )
こんにちは!金融ソリューション事業部の山下です。 本記事では、 こちらの記事 でも紹介した Unreal Engine が提供する「Pixel Streaiming」 プラグイン を使って AWS サーバーからリアルタイムCGストリーミング配信を行います。 説明をシンプルにする為、 AWS の基本的な知識(EC2, セキュリティグループ、IAMロールなど)の説明は割愛いたします。 また、本検証を行う場合、使用する GPU インスタンス の利用料金が発生する点にご注意ください。 実施手順 手順は、以下のとおりです。 AWS EC2で GPU インスタンス を作成 インスタンス 環境構築 UEプロジェクトのビルド UEアプリケーション、WebRTCサーバーの起動 Webブラウザ で接続確認 前提知識 1. Pixel Streaming (画像: https://docs.unrealengine.com/5.0/ja/overview-of-pixel-streaming-in-unreal-engine/ ) UEアプリケーションをサーバーサイドで実行して、 レンダリング 結果をWebRTCでストリーム配信する、EpicGames公式 プラグイン です。 インターネットに繋がった Webブラウザ があれば、 スマホ や タブレット 、ノートPCなど特別な端末スペック不要でUEアプリを楽しむことができます。 キーボード、マウス、タッチスクリーン入力などユーザーによるコン トロール も提供可能です。 使用環境/ツール Unreal Engine 5.0.3 Pixel Streaming plugin 1.0 AWS EC2 Instance type g4dn.xlarge AMI Ubuntu 20.04 LTS AMI NVIDIA CUDAドライバが必須 Architecture x86 EBS 250GB NICE DCV 2022.1 Client 手順1. AWS EC2で GPU インスタンス を作成 AWS EC2コンソールから インスタンス を作成します。 AMIには、 Ubuntu 20.04 LTS AMIを選択します。 このAMIには、PixelStreamingの動作要件として必要となる NVIDIA のCUDAドライバーがプリインストールされています。 インスタンス タイプは、 NVIDIA GPU が利用可能な G4dnインスタンス を選択します。 今回は動作検証が目的の為、最小サイズのxlargeを選択しました。 これから実施する Unreal Engine の コンパイル やアプリケーションのビルドにあまり時間をかけたくない方は、より高性能な インスタンス タイプをおすすめします。 また、vCPU使用リミットの引き上げが必要になる場合、 AWS に事前に申請が必要になります(今回選択したg4dn.xlargeの場合、vCPUは"4"になります)。 インスタンス の作成が完了したら、IAMロールでS3へのRead Permisionを付与します( AWS がEC2向けに提供する NVIDIA ドライバをダウンロードする為に必要となります)。 また、HTTPと SSH アクセス以外に、NICE DCV接続用の UDP / TCP ポートも開放します。 (※上記は検証用の設定なので、本番環境には使用しないでください。一応最低限のセキュリティとして SSH 接続にはkey pair、およびNICE DCV接続にはユーザー名とパスワードおよびセッションIDが必要になります) 手順2. インスタンス 環境構築 インスタンス に SSH などで接続して、環境構築を実施します。 こちらの AWS for Games Blogのチュートリアル を参考に、以下を実施します。 Ubuntu desctop managerのインストール NVIDIA ドライバのインストール DCVサーバーの起動、セッションの開始 AWS SecretsManagerを使用した、 Ubuntu ユーザーのパスワード生成 実行コマンド: #!/bin/bash #Installing the Ubuntu desktop manager sudo apt update -y sudo apt install -y ubuntu-desktop sudo apt install x11-xserver-utils sudo apt install awscli -y sudo apt install -y dpkg-dev sudo systemctl restart gdm3 sudo apt-get install xorg-dev -y #............................ # Disable the Wayland protocol. NICE DCV doesn't support the Wayland protocol. sudo sed -i '/WaylandEnable/s/^#//g' /etc/gdm3/custom.conf # ............................. # Install NVIDIA Drivers #.............................. sudo apt-get install -y unzip gcc make linux-headers-$(uname -r) cat << EOF | sudo tee --append /etc/modprobe.d/blacklist.conf blacklist vga16fb blacklist nouveau blacklist rivafb blacklist nvidiafb blacklist rivatv EOF # ........ sudo sed -i -e '$a GRUB_CMDLINE_LINUX="rdblacklist=nouveau"' /etc/default/grub sudo update-grub # Make sure to give the EC2 instance S3 read permissions, otherwise this will fail sudo aws s3 cp --recursive s3://ec2-linux-nvidia-drivers/latest/ . sudo chmod +x NVIDIA-Linux-x86_64*.run sudo /bin/sh ./NVIDIA-Linux-x86_64*.run nvidia-smi -q | head# Configure the X Server sudo systemctl set-default graphical.target sudo systemctl isolate graphical.target sudo apt install -y mesa-utils sudo init 3 sudo init 5 sudo nvidia-xconfig --preserve-busid --enable-all-gpus sudo DISPLAY=:0 XAUTHORITY=$(ps aux | grep "X.*\-auth" | grep -v grep \ | sed -n 's/.*-auth \([^ ]\+\).*/\1/p') glxinfo | grep -i "opengl.*version" sudo systemctl isolate multi-user.target sudo systemctl isolate graphical.target# Install DCV wget https://d1uj6qtbmh3dt5.cloudfront.net/NICE-GPG-KEY gpg --import NICE-GPG-KEY sudo rm NICE-GPG-KEY sudo dcvstartx & sudo wget https://d1uj6qtbmh3dt5.cloudfront.net/2022.0/Servers/nice-dcv-2022.0-12123-ubuntu2004-x86_64.tgz sudo tar xvzf nice-dcv-*ubun*.tgz && cd nice-dcv-*64 sudo apt install -y ./nice-dcv-ser*.deb sudo apt install -y ./nice-x*.deb sudo apt install -y ./nice-dcv-web*.deb sudo usermod -aG video ubuntu sudo usermod -aG video dcv # Start the DCV Server and DCV session sudo systemctl enable dcvserver sudo systemctl start dcvserver sudo dcv create-session --type=console --owner=ubuntu newsession sudo dcv list-sessions #sudo dcv close-session newsession # Create random password for the user Ubuntu PASS=$(aws secretsmanager get-random-password --require-each-included-type --password-length 20 --query RandomPassword) INSTANCE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/public-hostname) aws secretsmanager create-secret --name DCV/$INSTANCE --description "Credentials for $INSTANCE." --secret-string "{\"user\":\"ubuntu\",\"password\":$PASS}" sudo systemctl isolate multi-user.target sudo systemctl isolate graphical.target ( 参考URL ) 環境構築が完了したら、DCVサーバーとセッションが立ち上がっているはずです。 事前にダウンロードした(DCV Client) https://download.nice-dcv.com/ を起動して、 インスタンス に リモートデスクトップ で接続しましょう。 NICE DCV接続に必要な情報として、以下の情報が必要になります。 サーバーのホスト名 or IP ポート番号 (手順2.で設定した)セッションID (手順2.で設定した) インスタンス ユーザー名/パスワード 以下の構文で入力します。 server_hostname_or_IP:port#session_id この リモートデスクトップ 接続は、UEプロジェクトのビルドの為に行うものです。 その為、もし事前に Linux 向けビルド済のUEアプリケーションをお持ちの方は、次の手順3.は不要です。 手順3. UEプロジェクトのビルド Unreal Engine の GitHubレポジトリ より、 Unreal Engine をダウンロードして コンパイル します。 クローン: git clone git@github.com:EpicGames/UnrealEngine.git コンパイル : cd UnrealEngine/ ./Setup.sh ./GenerateProjectFiles.sh make UnrealEngine GitHub レポジトリへのアクセスには、EpicGamesアカウントと GitHub アカウントとの紐付けが事前に必要になります。 もしお済みでない方は、 EpicGames公式サイトのFAQ よりアカウントの紐付けを行ってください。 こちらの コンパイル には物凄く時間がかかります。私が行った際は数時間程度かかったので、一旦放置しましょう。 無事 コンパイル が完了したら、 Unreal Editorを立ち上げます。 cd Engine/Binaries/Linux/ ./UnrealEditor デモ用に、Architectureテンプレートから「Collab Viewer」テンプレートを選択します。 PixelStreaming用に、プロジェクト設定とビルドを行います。 詳細な解説は、 こちらのUnreal Engine公式Documentation を参考にしてください。 Pixel Streaming プラグイン の選択、再起動 メニュー「Edit」 -> 「Plugins」より、Pixel Streamingを検索して、 チェックボックス をONにします。 再起動が求められるので、実施します。 Additional Launch Parametersの設定 メニュー「Edit」 -> 「Editor Preferences」 -> 「Level Editor」 -> 「Play」カテゴリより、 Additional Launch Parametersに以下を入力して、音声とIP/ポートを設定します。 AudioMixer -PixelStreamingIP=localhost -PixelStreamingPort=8888 UEアプリの Linux 向けビルド メニュー「Platforms」 -> 「 Linux 」 -> 「Package Project」より、プロジェクトのビルドを行います。 適切な ディレクト リを設定してください( このビルドにもかなり時間がかかるので、ご注意ください)。 ビルドが完了したら、指定した ディレクト リに「 Linux 」フォルダが作成されます。 手順4. UEアプリケーション、WebRTCサーバーの起動 まず、WebRTCサーバーを立ち上げます。 Unreal Engine が提供する「SignallingWebServer」(Node.jsベースの Cirrus.js を使用)を起動します。 セットアップ: cd ~/Desktop/UnrealEngine/Samples/PixelStreaming/WebServers/SignallingWebServer/platform_scripts/bash chmod +x ./setup.sh ./setup.sh SignallingServerの起動: chmod +x ./Start_SignallingServer.sh ./Start_SignallingServer.sh 次に、UEアプリケーションを立ち上げます。 ビルド先の ディレクト リに移動して、プロジェクト名のついたファイル(今回は「ps_test」)を実行します。 オプションとして、UEアプリのヘッドレス起動、Pixel StreamingのIP/ポートを指定します。   cd ~/Desktop/Linux chmod +x ./ps_test.sh ./ps_test.sh -RenderOffScreen -PixelStreamingIP=127.0.0.1 -PixelStreamingPort=888 無事に両方のサーバーが起動すると、以下のログ(「Streamer connected: ::ffff: 127.0.0.1 」)が表示されます。 無事起動できたら、接続確認をしましょう! 手順5. Webブラウザ で接続確認 インスタンス のグローパル IPアドレス を指定して、お好きな Webブラウザ からアクセスします。 モバイルでも問題なく動きます。 終わりに 今回、 Unreal Engine 5アプリケーションを、 Webブラウザ 向けにリアルタイムストリーミング配信を行いました。 以前の記事 でも紹介したように、 Unreal Engine は非常に高精細なCGをリアルタイム レンダリング できる為、いわゆる" ノンゲーム "領域への活用も今後進んでいくと思われます。 課題を挙げるとすると、サーバーにある程度スペックの高い GPU マシンが必要になる為、コストが一定かかってしまう点です(今回の検証では、 AWS 利用料金が約12ドル程かかりました)。 また、 ゲームエンジン 以外に クラウド やWebの専門知識を必要とする点も、課題として挙げられます。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) TODO コンテナ化 スケーリング セルフヒーリング ネットワーク構成、各種セキュリティ CI/CD 参考 White Paper: Streaming Unreal Engine content to multiple platforms AWS for Games Blog: Unreal Engine Pixel Streaming in AWS with Ubuntu OS Github: aws-samples/deploying-unreal-engine-pixel-streaming-server-on-ec2 執筆: @yamashita.yuki 、レビュー: @mizuno.kazuhiro ( Shodo で執筆されました )
こんにちは!金融ソリューション事業部の山下です。 本記事では、 こちらの記事 でも紹介した Unreal Engine が提供する「Pixel Streaiming」 プラグイン を使って AWS サーバーからリアルタイムCGストリーミング配信を行います。 説明をシンプルにする為、 AWS の基本的な知識(EC2, セキュリティグループ、IAMロールなど)の説明は割愛いたします。 また、本検証を行う場合、使用する GPU インスタンス の利用料金が発生する点にご注意ください。 実施手順 手順は、以下のとおりです。 AWS EC2で GPU インスタンス を作成 インスタンス 環境構築 UEプロジェクトのビルド UEアプリケーション、WebRTCサーバーの起動 Webブラウザ で接続確認 前提知識 1. Pixel Streaming (画像: https://docs.unrealengine.com/5.0/ja/overview-of-pixel-streaming-in-unreal-engine/ ) UEアプリケーションをサーバーサイドで実行して、 レンダリング 結果をWebRTCでストリーム配信する、EpicGames公式 プラグイン です。 インターネットに繋がった Webブラウザ があれば、 スマホ や タブレット 、ノートPCなど特別な端末スペック不要でUEアプリを楽しむことができます。 キーボード、マウス、タッチスクリーン入力などユーザーによるコン トロール も提供可能です。 使用環境/ツール Unreal Engine 5.0.3 Pixel Streaming plugin 1.0 AWS EC2 Instance type g4dn.xlarge AMI Ubuntu 20.04 LTS AMI NVIDIA CUDAドライバが必須 Architecture x86 EBS 250GB NICE DCV 2022.1 Client 手順1. AWS EC2で GPU インスタンス を作成 AWS EC2コンソールから インスタンス を作成します。 AMIには、 Ubuntu 20.04 LTS AMIを選択します。 このAMIには、PixelStreamingの動作要件として必要となる NVIDIA のCUDAドライバーがプリインストールされています。 インスタンス タイプは、 NVIDIA GPU が利用可能な G4dnインスタンス を選択します。 今回は動作検証が目的の為、最小サイズのxlargeを選択しました。 これから実施する Unreal Engine の コンパイル やアプリケーションのビルドにあまり時間をかけたくない方は、より高性能な インスタンス タイプをおすすめします。 また、vCPU使用リミットの引き上げが必要になる場合、 AWS に事前に申請が必要になります(今回選択したg4dn.xlargeの場合、vCPUは"4"になります)。 インスタンス の作成が完了したら、IAMロールでS3へのRead Permisionを付与します( AWS がEC2向けに提供する NVIDIA ドライバをダウンロードする為に必要となります)。 また、HTTPと SSH アクセス以外に、NICE DCV接続用の UDP / TCP ポートも開放します。 (※上記は検証用の設定なので、本番環境には使用しないでください。一応最低限のセキュリティとして SSH 接続にはkey pair、およびNICE DCV接続にはユーザー名とパスワードおよびセッションIDが必要になります) 手順2. インスタンス 環境構築 インスタンス に SSH などで接続して、環境構築を実施します。 こちらの AWS for Games Blogのチュートリアル を参考に、以下を実施します。 Ubuntu desctop managerのインストール NVIDIA ドライバのインストール DCVサーバーの起動、セッションの開始 AWS SecretsManagerを使用した、 Ubuntu ユーザーのパスワード生成 実行コマンド: #!/bin/bash #Installing the Ubuntu desktop manager sudo apt update -y sudo apt install -y ubuntu-desktop sudo apt install x11-xserver-utils sudo apt install awscli -y sudo apt install -y dpkg-dev sudo systemctl restart gdm3 sudo apt-get install xorg-dev -y #............................ # Disable the Wayland protocol. NICE DCV doesn't support the Wayland protocol. sudo sed -i '/WaylandEnable/s/^#//g' /etc/gdm3/custom.conf # ............................. # Install NVIDIA Drivers #.............................. sudo apt-get install -y unzip gcc make linux-headers-$(uname -r) cat << EOF | sudo tee --append /etc/modprobe.d/blacklist.conf blacklist vga16fb blacklist nouveau blacklist rivafb blacklist nvidiafb blacklist rivatv EOF # ........ sudo sed -i -e '$a GRUB_CMDLINE_LINUX="rdblacklist=nouveau"' /etc/default/grub sudo update-grub # Make sure to give the EC2 instance S3 read permissions, otherwise this will fail sudo aws s3 cp --recursive s3://ec2-linux-nvidia-drivers/latest/ . sudo chmod +x NVIDIA-Linux-x86_64*.run sudo /bin/sh ./NVIDIA-Linux-x86_64*.run nvidia-smi -q | head# Configure the X Server sudo systemctl set-default graphical.target sudo systemctl isolate graphical.target sudo apt install -y mesa-utils sudo init 3 sudo init 5 sudo nvidia-xconfig --preserve-busid --enable-all-gpus sudo DISPLAY=:0 XAUTHORITY=$(ps aux | grep "X.*\-auth" | grep -v grep \ | sed -n 's/.*-auth \([^ ]\+\).*/\1/p') glxinfo | grep -i "opengl.*version" sudo systemctl isolate multi-user.target sudo systemctl isolate graphical.target# Install DCV wget https://d1uj6qtbmh3dt5.cloudfront.net/NICE-GPG-KEY gpg --import NICE-GPG-KEY sudo rm NICE-GPG-KEY sudo dcvstartx & sudo wget https://d1uj6qtbmh3dt5.cloudfront.net/2022.0/Servers/nice-dcv-2022.0-12123-ubuntu2004-x86_64.tgz sudo tar xvzf nice-dcv-*ubun*.tgz && cd nice-dcv-*64 sudo apt install -y ./nice-dcv-ser*.deb sudo apt install -y ./nice-x*.deb sudo apt install -y ./nice-dcv-web*.deb sudo usermod -aG video ubuntu sudo usermod -aG video dcv # Start the DCV Server and DCV session sudo systemctl enable dcvserver sudo systemctl start dcvserver sudo dcv create-session --type=console --owner=ubuntu newsession sudo dcv list-sessions #sudo dcv close-session newsession # Create random password for the user Ubuntu PASS=$(aws secretsmanager get-random-password --require-each-included-type --password-length 20 --query RandomPassword) INSTANCE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/public-hostname) aws secretsmanager create-secret --name DCV/$INSTANCE --description "Credentials for $INSTANCE." --secret-string "{\"user\":\"ubuntu\",\"password\":$PASS}" sudo systemctl isolate multi-user.target sudo systemctl isolate graphical.target ( 参考URL ) 環境構築が完了したら、DCVサーバーとセッションが立ち上がっているはずです。 事前にダウンロードした(DCV Client) https://download.nice-dcv.com/ を起動して、 インスタンス に リモートデスクトップ で接続しましょう。 NICE DCV接続に必要な情報として、以下の情報が必要になります。 サーバーのホスト名 or IP ポート番号 (手順2.で設定した)セッションID (手順2.で設定した) インスタンス ユーザー名/パスワード 以下の構文で入力します。 server_hostname_or_IP:port#session_id この リモートデスクトップ 接続は、UEプロジェクトのビルドの為に行うものです。 その為、もし事前に Linux 向けビルド済のUEアプリケーションをお持ちの方は、次の手順3.は不要です。 手順3. UEプロジェクトのビルド Unreal Engine の GitHubレポジトリ より、 Unreal Engine をダウンロードして コンパイル します。 クローン: git clone git@github.com:EpicGames/UnrealEngine.git コンパイル : cd UnrealEngine/ ./Setup.sh ./GenerateProjectFiles.sh make UnrealEngine GitHub レポジトリへのアクセスには、EpicGamesアカウントと GitHub アカウントとの紐付けが事前に必要になります。 もしお済みでない方は、 EpicGames公式サイトのFAQ よりアカウントの紐付けを行ってください。 こちらの コンパイル には物凄く時間がかかります。私が行った際は数時間程度かかったので、一旦放置しましょう。 無事 コンパイル が完了したら、 Unreal Editorを立ち上げます。 cd Engine/Binaries/Linux/ ./UnrealEditor デモ用に、Architectureテンプレートから「Collab Viewer」テンプレートを選択します。 PixelStreaming用に、プロジェクト設定とビルドを行います。 詳細な解説は、 こちらのUnreal Engine公式Documentation を参考にしてください。 Pixel Streaming プラグイン の選択、再起動 メニュー「Edit」 -> 「Plugins」より、Pixel Streamingを検索して、 チェックボックス をONにします。 再起動が求められるので、実施します。 Additional Launch Parametersの設定 メニュー「Edit」 -> 「Editor Preferences」 -> 「Level Editor」 -> 「Play」カテゴリより、 Additional Launch Parametersに以下を入力して、音声とIP/ポートを設定します。 AudioMixer -PixelStreamingIP=localhost -PixelStreamingPort=8888 UEアプリの Linux 向けビルド メニュー「Platforms」 -> 「 Linux 」 -> 「Package Project」より、プロジェクトのビルドを行います。 適切な ディレクト リを設定してください( このビルドにもかなり時間がかかるので、ご注意ください)。 ビルドが完了したら、指定した ディレクト リに「 Linux 」フォルダが作成されます。 手順4. UEアプリケーション、WebRTCサーバーの起動 まず、WebRTCサーバーを立ち上げます。 Unreal Engine が提供する「SignallingWebServer」(Node.jsベースの Cirrus.js を使用)を起動します。 セットアップ: cd ~/Desktop/UnrealEngine/Samples/PixelStreaming/WebServers/SignallingWebServer/platform_scripts/bash chmod +x ./setup.sh ./setup.sh SignallingServerの起動: chmod +x ./Start_SignallingServer.sh ./Start_SignallingServer.sh 次に、UEアプリケーションを立ち上げます。 ビルド先の ディレクト リに移動して、プロジェクト名のついたファイル(今回は「ps_test」)を実行します。 オプションとして、UEアプリのヘッドレス起動、Pixel StreamingのIP/ポートを指定します。   cd ~/Desktop/Linux chmod +x ./ps_test.sh ./ps_test.sh -RenderOffScreen -PixelStreamingIP=127.0.0.1 -PixelStreamingPort=888 無事に両方のサーバーが起動すると、以下のログ(「Streamer connected: ::ffff: 127.0.0.1 」)が表示されます。 無事起動できたら、接続確認をしましょう! 手順5. Webブラウザ で接続確認 インスタンス のグローパル IPアドレス を指定して、お好きな Webブラウザ からアクセスします。 モバイルでも問題なく動きます。 終わりに 今回、 Unreal Engine 5アプリケーションを、 Webブラウザ 向けにリアルタイムストリーミング配信を行いました。 以前の記事 でも紹介したように、 Unreal Engine は非常に高精細なCGをリアルタイム レンダリング できる為、いわゆる" ノンゲーム "領域への活用も今後進んでいくと思われます。 課題を挙げるとすると、サーバーにある程度スペックの高い GPU マシンが必要になる為、コストが一定かかってしまう点です(今回の検証では、 AWS 利用料金が約12ドル程かかりました)。 また、 ゲームエンジン 以外に クラウド やWebの専門知識を必要とする点も、課題として挙げられます。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) TODO コンテナ化 スケーリング セルフヒーリング ネットワーク構成、各種セキュリティ CI/CD 参考 White Paper: Streaming Unreal Engine content to multiple platforms AWS for Games Blog: Unreal Engine Pixel Streaming in AWS with Ubuntu OS Github: aws-samples/deploying-unreal-engine-pixel-streaming-server-on-ec2 執筆: @yamashita.yuki 、レビュー: @mizuno.kazuhiro ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 AWS アカウントのセキュリティを向上させるために、IAMユーザー作成のイベントに即時反応して自動削除する仕組みを作りました。それに際し、マネジメントコンソールからIAMユーザーを作成・削除した時に裏側で起こっていることも調べてみました。 IAMユーザーをなるべく減らしたい理由 運用ルールだけではなく仕組みを 構成図 ユーザーを削除するために必要なこと コンソールからIAMユーザーを作成するときに起こっていること CreateUser (A)CreateLoginProfile (B)CreateAccessKey (G)PutUserPolicy (H)AttachUserPolicy (I)AddUserToGroup コンソールからIAMユーザーを削除するときに起こっていること (A)DeleteLoginProfile (B)ListAccessKeys > DeleteAccessKey (C)ListSigningCertificates > DeleteSigningCertificate (D)ListSSHPublicKeys > DeleteSSHPublicKey (E)ListServiceSpecificCredentials > DeleteServiceSpecificCredential (F)ListMFADevices > DeactivateMFADevice > DeleteVirtualMFADevice (G)ListUserPolicies > DeleteUserPolicy (H)ListAttachedUserPolicies > DetachUserPolicy (I)ListGroupsForUser > RemoveUserFromGroup DeleteUser 今回の仕組みでカバーする範囲 CDKテンプレート Lambda関数 パッケージからのインポートとIAMClientの生成 (A)ログインプロファイルの削除 (B)アクセスキーの削除 (G)インラインポリシーの削除 (H)管理ポリシーのデタッチ (I)IAMグループへの所属解除 Lambdaハンドラ さいごに IAMユーザーをなるべく減らしたい理由 IAMユーザーはログ インパス ワードやアクセスキーなどを利用して AWS リソースにアクセスできますが、いずれも長期的な認証情報です。有効期限が存在しないので退職者管理が必要となり、漏えいした際の影響も大きいため、 AWS アカウントに存在するIAMユーザーをなるべく減らすのが良いでしょう。IAMユーザーを使わずに AWS リソースにアクセスする方法には、例えば以下のようなものがあります。 AWS リソースから別の AWS リソースにアクセスする場合、短期的な認証情報を発行するIAMロールを割り当てる マネジメントコンソールにアクセスしたい場合、 SAML 2.0やIDフェデレーションでIAMロールを引き受けてシングルサインオンする 外部サービスから AWS リソースにアクセスする場合、サポートされていればOIDCなどでIAMロールを引き受ける(例: GitHubの場合 ) 開発中に一時的にアクセスキーを使いたいなど、IAMユーザーが欲しいケースもありますが、むやみやたらには増やさない方が良いでしょう。 運用ルールだけではなく仕組みを 決まったIAMユーザー以外は作成しないことを運用ルールとして定めている場合も多いと思いますが、ルールはあくまでもルール。あらゆる状況において徹底を保証するものではありません。新任者にルールを漏れなく周知することも課題になります。また何らかのルートで悪意ある第 三者 にアカウントに侵入されてしまった場合、 バックドア としてIAMユーザーを作られることもあります。運用ルールだけではなく、実際のリソースとしてIAMユーザーが作成されたときに仕組みで対応できると安心でしょう。今回は強行策として、ユーザーの作成を検知してほぼリアルタイムで自動削除を行う仕組みを作りました。 構成図 ユーザーの削除はEventBridgeのイベントをトリガーにLambda関数が行います。IAMイベントは直接EventBridgeイベントを生成しないため、 CloudTrailのAPIコールでイベントルールが発火 するようにします。IAMの API コールは us-east-1 リージョンに記録されるため、EventBridgeルールやLambda関数も us-east-1 リージョンに作ります。ユーザー作成イベントドリブンのため、既存のユーザーには影響がありません。 この記事の後半に、この仕組みのCDKコードとLambda関数の実装を掲載しています。 ユーザーを削除するために必要なこと IAMユーザーを削除するには DeleteUser API を使うだけで良さそうだと思っていましたが、実はそれほど単純ではありませんでした。 API のリンク先の説明にある通り、 DeleteUser の前にそのIAMユーザーに付属する以下のアイテムを全て削除する 必要があります。それぞれの説明と削除するための API 名を箇条書きします。また便宜上、(A)~(I)の記号を付け、この記事を通して同じ記号を使うようにしています。 (A)ログインプロファイル ( DeleteLoginProfile ) マネジメントコンソールにログインするためのパスワード。1ユーザーにつき1つのみ作成できる (B)アクセスキー ( DeleteAccessKey ) アクセスキーIDとシークレットアクセスキーのペア。1ユーザーあたり2つまで発行できる (C)署 名証 明書 ( DeleteSigningCertificate ) SOAP アクセスなどで使用するX.509証明書。1ユーザーあたり複数登録できる (D) SSH 公開鍵 ( DeleteSSHPublicKey ) CodeCommit 用の SSH 公開鍵。1ユーザーあたり複数登録できる (E)各サービス用のクレデンシャル ( DeleteServiceSpecificCredential ) CodeCommitや Amazon Keyspaces用のクレデンシャル。1ユーザーあたり複数発行できる (F)MFAデ バイス ( DeactivateMFADevice DeleteVirtualMFADevice ) サインイン時に使用するMFAデ バイス 。 DeactivateMFADevice で無効化してから DeleteVirtualMFADevice で削除する必要がある (G)インラインポリシー ( DeleteUserPolicy ) ユーザーに追加されているインラインポリシー。1ユーザーあたり複数作成できる (H)アタッチされている管理ポリシー ( DetachUserPolicy ) アタッチされている管理ポリシー。1ユーザーあたり複数アタッチできる (I)IAMグループへの所属 ( RemoveUserFromGroup ) 1ユーザーあたり複数のグループに所属できる なんと9種類もあります。(C)(D)(E)あたりは使われるケースが少ないと思いますが、他はいずれも馴染み深いものですね。先にこれらのアイテムを削除しないと、 DeleteUser の呼び出しは失敗してしまいます。 コンソールからIAMユーザーを作成するときに起こっていること IAMユーザーと9つのアイテムとの関連付けについての理解を深めるために、マネジメントコンソールからIAMユーザーを作成する時に、裏側でどのような API が呼ばれているのかを掘り下げてみます。コンソールでユーザーを作る時にはいろいろなオプションがありますが、us-east-1 リージョンの CloudTrail ログより実際に呼び出された API をそれぞれ調べました。参照系の API も多く呼ばれていますが、更新系 API のみに着目します。 CreateUser 最初に必ず CreateUser API が呼ばれます。コンソールからユーザーを作成するときはパスワードかアクセスキーのどちらかを同時に作らなければなりませんが、実はこの API 単体ではいずれも生成されません。この時点ではコンソールアクセスもプログラムアクセスもできないユーザー本体のみができあがります。 これ以降の API コールはコンソールで選択した内容によって変わります。いずれも独立したリソース作成のため、実行順は重要ではありません。 (A)CreateLoginProfile 認証情報として「パスワード - AWS マネジメントコンソールへのアクセス」を選択しているとこの API が呼ばれ、パスワードが設定されます。 (B)CreateAccessKey 認証情報として「アクセスキー - プログラムによるアクセス」を選択しているとこの API が呼ばれ、アクセスキーが発行されます。 (G)PutUserPolicy アクセス許可の設定で「アクセス権限を既存のユーザーからコピー」でコピー元のユーザーにインラインポリシーがあるとこの API が呼ばれ、同じインラインポリシーが追加されます。複数のポリシーを追加可能です。 (H)AttachUserPolicy アクセス許可の設定で「既存のポリシーを直接アタッチ」する、もしくは「アクセス権限を既存のユーザーからコピー」でコピー元のユーザーに管理ポリシーがアタッチされているとこの API が呼ばれ、ユーザーに管理ポリシーがアタッチされます。複数のポリシーをアタッチ可能です。 (I)AddUserToGroup アクセス許可の設定で「ユーザーをグループに追加」する、もしくは「アクセス権限を既存のユーザーからコピー」でコピー元のユーザーがIAMグループに所属しているとこの API が呼ばれ、ユーザーがグループに所属します。複数のグループに所属可能です。 今のところ、「(C)署 名証 明書」「(D) SSH 公開鍵」「(E)各サービス用のクレデンシャル」「(F)MFAデ バイス 」はコンソールからのユーザー作成時に同時には作成できませんでした。 コンソールからIAMユーザーを削除するときに起こっていること 次に、(A)~(I)を全て追加したもりもりのユーザーを作成しておき、コンソールから削除した時にどのような API が裏側で呼ばれているのかを見てみました。更新系 API と、それに関連する参照系 API について記載します。 (A)DeleteLoginProfile パスワードが削除されます。 (B)ListAccessKeys > DeleteAccessKey まずは ListAccessKeys で削除対象ユーザーのアクセスキー一覧を取得し、見つかればそれぞれについて DeleteAccessKey で削除されます。 (C)ListSigningCertificates > DeleteSigningCertificate ListSigningCertificates で証明書一覧を取得し、見つかればそれぞれについて DeleteSigningCertificate で削除されます。 (D)ListSSHPublicKeys > DeleteSSHPublicKey ListSSHPublicKeys で鍵一覧を取得し、見つかればそれぞれについて DeleteSSHPublicKey で削除されます。 (E)ListServiceSpecificCredentials > DeleteServiceSpecificCredential ListServiceSpecificCredentials でクレデンシャル一覧を取得し、見つかればそれぞれについて DeleteServiceSpecificCredential で削除されます。 (F)ListMFADevices > DeactivateMFADevice > DeleteVirtualMFADevice ListMFADevices でデ バイス 一覧を取得し、見つかればそれぞれについて DeactivateMFADevice で無効化したのち、 DeleteVirtualMFADevice で削除されます。 (G)ListUserPolicies > DeleteUserPolicy ListUserPolicies でインラインポリシー一覧を取得し、見つかればそれぞれについて DeleteUserPolicy で削除されます。 (H)ListAttachedUserPolicies > DetachUserPolicy ListAttachedUserPolicies でアタッチされている管理ポリシー一覧を取得し、見つかればそれぞれについて DetachUserPolicy でデタッチされます。管理ポリシー自体は削除されません。 (I)ListGroupsForUser > RemoveUserFromGroup ListGroupsForUser で所属しているIAMグループ一覧を取得し、見つかればそれぞれについて RemoveUserFromGroup で所属が解除されます。IAMグループ自体は削除されません。 DeleteUser 以上のアイテムが全て削除されると、最後に DeleteUser が呼ばれてユーザー本体が晴れて(?)削除されます。画面では数クリックで終わる操作ですが、実は多くの API が呼ばれていることがわかりますね。 今回の仕組みでカバーする範囲 ユーザー削除時の API コールを参考にして、IAMユーザーを自動削除するための仕組みを作ります。以下ではCDKとLambda関数のコードを解説しますが、今回はIAMユーザー本体の削除に先立って、以下の5つのアイテムの削除のみを行います。 (A)ログインプロファイル (B)アクセスキー (G)インラインポリシー (H)アタッチされている管理ポリシー (I)IAMグループへの所属 (C)(D)(E)(F)についてはコンソールでユーザーを作成する時に同時に作成できないため、今回は省略しています。コンソールでも CLI でも、これらのアイテムを作成するためにはユーザー作成とは別の操作が必要になりますので、その操作が実施される前にEventBridgeイベントが発火し、ユーザーが削除されることが期待できます。もちろん、 CLI などから CreateUser 実行後、EventBridgeイベントが発火する前に(C)(D)(E)(F)を作成する API が素早く実行されるとユーザーの削除に失敗しますので、より確実にユーザーを削除したい場合は(C)(D)(E)(F)の削除も対応した方が良いでしょう。 CDKテンプレート IAMユーザーの作成を検知し、自動削除する仕組みのCDKスタックは次のとおりです。 EventBridgeルールは detailType を AWS API Call via CloudTrail 、 eventName を CreateUser にすることで、ユーザー作成 API の呼び出しがCloudTrailに記録されるとイベントが起動します。 前述の通り、IAMの更新系 API は us-east-1 リージョンのCloudTrailに記録されるので、このスタックも us-east-1 リージョンにデプロイする必要があります。 import { Duration , Stack , StackProps } from "aws-cdk-lib" ; import * as events from "aws-cdk-lib/aws-events" ; import * as eventTargets from "aws-cdk-lib/aws-events-targets" ; import * as iam from "aws-cdk-lib/aws-iam" ; import { Runtime } from "aws-cdk-lib/aws-lambda" ; import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs" ; import { Construct } from "constructs" ; export class DeleteUserStack extends Stack { constructor( scope: Construct , id: string , props?: StackProps ) { super( scope , id , props ); // Lambda関数 const deleteUserFunction = new lambdaNodejs.NodejsFunction ( this , "DeleteUserFunction" , { entry: "functions/delete-user.ts" , runtime: Runtime.NODEJS_16_X , timeout: Duration.minutes ( 15 ), } ); // 必要なActionを追加 deleteUserFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ "*" ] , actions: [ "iam:DeleteUser" , "iam:ListAccessKeys" , "iam:DeleteAccessKey" , "iam:ListGroupsForUser" , "iam:RemoveUserFromGroup" , "iam:GetLoginProfile" , "iam:DeleteLoginProfile" , "iam:ListAttachedUserPolicies" , "iam:DetachUserPolicy" , "iam:ListUserPolicies" , "iam:DeleteUserPolicy" , ] , effect: iam.Effect.ALLOW , } ) ); // EventBrdigeルール new events.Rule ( this , "CreateUserEventRule" , { ruleName: "create-user" , eventPattern: { source: [ "aws.iam" ] , detailType: [ "AWS API Call via CloudTrail" ] , detail: { eventSource: [ "iam.amazonaws.com" ] , eventName: [ "CreateUser" ] , } , } , targets: [ new eventTargets.LambdaFunction ( deleteUserFunction ) ] , } ); } } Lambda関数 functions/delete-user.ts のLambda関数の実装を説明します。 パッケージからのインポートとIAMClientの生成 IAM用の SDK をインポートします。Lambda関数が受け取るのはEventBridgeイベントなので、 aws-lambda パッケージから EventBridgeEvent もインポートします。 IAMClientは us-east-1 リージョン向けに生成します。 import * as iam from "@aws-sdk/client-iam" ; import { Handler , EventBridgeEvent } from "aws-lambda" ; const client = new iam.IAMClient ( { region: "us-east-1" } ); (A)ログインプロファイルの削除 ログインプロファイルを削除する関数です。最初に GetLoginProfileCommand でログインプロファイルが存在するかどうかを確認し、存在すれば while ループで削除を試みます。while ループにしている理由は、プロファイルの作成には時間がかかるようで、 CreateLoginProfile コール直後に DeleteLoginProfile をしても次のエラーになってしまうためです。 Login Profile for User <ユーザー名> cannot be modified while login profile is being created. そこで while ループで間に5秒間ずつ待機し、最大30回削除を試みる実装にしました。実際にやってみると、体感的にログインプロファイル作成後15秒ほどで削除できる状態になっていました。 export const deleteLoginProfile = async ( userName: string ) : Promise < void > => { try { const { LoginProfile } = await client.send (new iam.GetLoginProfileCommand ( { UserName: userName } )); if ( LoginProfile === undefined || LoginProfile.UserName === undefined ) return; } catch ( e: unknown ) { console .log ( ` ${ e instanceof Error ? e.message : "" } ` ); return; } let attempts = 30 ; while ( attempts > 0 ) { try { await client.send (new iam.DeleteLoginProfileCommand ( { UserName: userName } )); break; } catch ( e: unknown ) { console .log ( ` ${ e instanceof Error ? e.message : "" } ` ); } await new Promise (( resolve ) => setTimeout ( resolve , 5000 )); attempts --; } } ; (B)アクセスキーの削除 次の関数でアクセスキーを削除します。まずは ListAccessKeysCommand でアクセスキー一覧を取得します。アクセスキーの件数が多い場合(実際には各ユーザー2つまでなので起こらないと思いますが)、一回の ListAccessKeysCommand 呼び出しで全件取得できず、結果の IsTruncated プロパティが true で返ってきます。この時の Marker プロパティを次の ListAccessKeysCommand 呼び出しのパラメータに含めることで、続きの結果を取得できます。一覧取得後は DeleteAccessKeyCommand で1つずつ削除します。 export const deleteAccessKeys = async ( userName: string ) : Promise < void > => { let accessKeyIds: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListAccessKeysCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListAccessKeysCommand ( params )); if ( output.AccessKeyMetadata ) { accessKeyIds = accessKeyIds.concat ( output.AccessKeyMetadata.filter (( accessKeyMetadata ) => accessKeyMetadata.AccessKeyId ) .map ( ( accessKeyMetadata ) => accessKeyMetadata.AccessKeyId ! ) ); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( accessKeyIds.length === 0 ) return; await Promise . all( accessKeyIds.map (async ( keyId ) => { await client.send (new iam.DeleteAccessKeyCommand ( { UserName: userName , AccessKeyId: keyId } )); } ) ); } ; (G)インラインポリシーの削除 (B)アクセスキーの削除 と同様の処理なので、説明は割愛します。 export const deleteUserPolicies = async ( userName: string ) : Promise < void > => { let policyNames: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListUserPoliciesCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListUserPoliciesCommand ( params )); if ( output.PolicyNames ) { policyNames = policyNames.concat ( output.PolicyNames.filter (( name ) => name )); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( policyNames.length === 0 ) return; await Promise . all( policyNames.map (async ( name ) => { await client.send (new iam.DeleteUserPolicyCommand ( { UserName: userName , PolicyName: name } )); } ) ); } ; (H)管理ポリシーのデタッチ 同様の処理なので、説明は割愛します。 export const detachUserPolicies = async ( userName: string ) : Promise < void > => { let attachedPolicieArns: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListAttachedUserPoliciesCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListAttachedUserPoliciesCommand ( params )); if ( output.AttachedPolicies ) { attachedPolicieArns = attachedPolicieArns.concat ( output.AttachedPolicies.filter (( policy ) => policy.PolicyArn ) .map (( policy ) => policy.PolicyArn ! ) ); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( attachedPolicieArns.length === 0 ) return; await Promise . all( attachedPolicieArns.map (async ( arn ) => { await client.send (new iam.DetachUserPolicyCommand ( { UserName: userName , PolicyArn: arn } )); } ) ); } ; (I)IAMグループへの所属解除 同様の処理なので、説明は割愛します。 export const removeUserFromGroups = async ( userName: string ) : Promise < void > => { let groupNames: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListGroupsForUserCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListGroupsForUserCommand ( params )); if ( output.Groups ) { groupNames = groupNames.concat ( output.Groups.filter (( group ) => group.GroupName ) .map (( group ) => group.GroupName ! )); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( groupNames.length === 0 ) return; await Promise . all( groupNames.map (async ( groupName ) => { await client.send (new iam.RemoveUserFromGroupCommand ( { UserName: userName , GroupName: groupName } )); } ) ); } ; Lambdaハンドラ 作成されたIAMユーザー名をイベントの中身から取得するための型を定義し、上で説明した関数を利用してLambdaハンドラを実装します。各アイテムの削除後に DeleteUserCommand でユーザー本体を削除します。 type Detail = { responseElements: { user: { userName: string } } } ; export const handler: Handler = async ( event: EventBridgeEvent < string , Detail >) => { const userName = event.detail.responseElements.user.userName ; await Promise . all( [ deleteLoginProfile ( userName ), deleteAccessKeys ( userName ), deleteUserPolicies ( userName ), detachUserPolicies ( userName ), removeUserFromGroups ( userName ), ] ); await client.send (new iam.DeleteUserCommand ( { UserName: userName } )); } ; これで、IAMユーザーが作成されるイベントをトリガーに、即座にユーザーを削除する仕組みが完成しました。 さいごに 自前で自動化の仕組みを実装してみると、普段コンソールでしている操作の裏側でどのような API が動いているのかが分かり、とても面白かったです。 最後までお読みただいてありがとうございました。 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 - セキュリティエンジニア(セキュリティ設計) 執筆: @kou.kinyo2 、レビュー: @higa ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 AWS アカウントのセキュリティを向上させるために、IAMユーザー作成のイベントに即時反応して自動削除する仕組みを作りました。それに際し、マネジメントコンソールからIAMユーザーを作成・削除した時に裏側で起こっていることも調べてみました。 IAMユーザーをなるべく減らしたい理由 運用ルールだけではなく仕組みを 構成図 ユーザーを削除するために必要なこと コンソールからIAMユーザーを作成するときに起こっていること CreateUser (A)CreateLoginProfile (B)CreateAccessKey (G)PutUserPolicy (H)AttachUserPolicy (I)AddUserToGroup コンソールからIAMユーザーを削除するときに起こっていること (A)DeleteLoginProfile (B)ListAccessKeys > DeleteAccessKey (C)ListSigningCertificates > DeleteSigningCertificate (D)ListSSHPublicKeys > DeleteSSHPublicKey (E)ListServiceSpecificCredentials > DeleteServiceSpecificCredential (F)ListMFADevices > DeactivateMFADevice > DeleteVirtualMFADevice (G)ListUserPolicies > DeleteUserPolicy (H)ListAttachedUserPolicies > DetachUserPolicy (I)ListGroupsForUser > RemoveUserFromGroup DeleteUser 今回の仕組みでカバーする範囲 CDKテンプレート Lambda関数 パッケージからのインポートとIAMClientの生成 (A)ログインプロファイルの削除 (B)アクセスキーの削除 (G)インラインポリシーの削除 (H)管理ポリシーのデタッチ (I)IAMグループへの所属解除 Lambdaハンドラ さいごに IAMユーザーをなるべく減らしたい理由 IAMユーザーはログ インパス ワードやアクセスキーなどを利用して AWS リソースにアクセスできますが、いずれも長期的な認証情報です。有効期限が存在しないので退職者管理が必要となり、漏えいした際の影響も大きいため、 AWS アカウントに存在するIAMユーザーをなるべく減らすのが良いでしょう。IAMユーザーを使わずに AWS リソースにアクセスする方法には、例えば以下のようなものがあります。 AWS リソースから別の AWS リソースにアクセスする場合、短期的な認証情報を発行するIAMロールを割り当てる マネジメントコンソールにアクセスしたい場合、 SAML 2.0やIDフェデレーションでIAMロールを引き受けてシングルサインオンする 外部サービスから AWS リソースにアクセスする場合、サポートされていればOIDCなどでIAMロールを引き受ける(例: GitHubの場合 ) 開発中に一時的にアクセスキーを使いたいなど、IAMユーザーが欲しいケースもありますが、むやみやたらには増やさない方が良いでしょう。 運用ルールだけではなく仕組みを 決まったIAMユーザー以外は作成しないことを運用ルールとして定めている場合も多いと思いますが、ルールはあくまでもルール。あらゆる状況において徹底を保証するものではありません。新任者にルールを漏れなく周知することも課題になります。また何らかのルートで悪意ある第 三者 にアカウントに侵入されてしまった場合、 バックドア としてIAMユーザーを作られることもあります。運用ルールだけではなく、実際のリソースとしてIAMユーザーが作成されたときに仕組みで対応できると安心でしょう。今回は強行策として、ユーザーの作成を検知してほぼリアルタイムで自動削除を行う仕組みを作りました。 構成図 ユーザーの削除はEventBridgeのイベントをトリガーにLambda関数が行います。IAMイベントは直接EventBridgeイベントを生成しないため、 CloudTrailのAPIコールでイベントルールが発火 するようにします。IAMの API コールは us-east-1 リージョンに記録されるため、EventBridgeルールやLambda関数も us-east-1 リージョンに作ります。ユーザー作成イベントドリブンのため、既存のユーザーには影響がありません。 この記事の後半に、この仕組みのCDKコードとLambda関数の実装を掲載しています。 ユーザーを削除するために必要なこと IAMユーザーを削除するには DeleteUser API を使うだけで良さそうだと思っていましたが、実はそれほど単純ではありませんでした。 API のリンク先の説明にある通り、 DeleteUser の前にそのIAMユーザーに付属する以下のアイテムを全て削除する 必要があります。それぞれの説明と削除するための API 名を箇条書きします。また便宜上、(A)~(I)の記号を付け、この記事を通して同じ記号を使うようにしています。 (A)ログインプロファイル ( DeleteLoginProfile ) マネジメントコンソールにログインするためのパスワード。1ユーザーにつき1つのみ作成できる (B)アクセスキー ( DeleteAccessKey ) アクセスキーIDとシークレットアクセスキーのペア。1ユーザーあたり2つまで発行できる (C)署 名証 明書 ( DeleteSigningCertificate ) SOAP アクセスなどで使用するX.509証明書。1ユーザーあたり複数登録できる (D) SSH 公開鍵 ( DeleteSSHPublicKey ) CodeCommit 用の SSH 公開鍵。1ユーザーあたり複数登録できる (E)各サービス用のクレデンシャル ( DeleteServiceSpecificCredential ) CodeCommitや Amazon Keyspaces用のクレデンシャル。1ユーザーあたり複数発行できる (F)MFAデ バイス ( DeactivateMFADevice DeleteVirtualMFADevice ) サインイン時に使用するMFAデ バイス 。 DeactivateMFADevice で無効化してから DeleteVirtualMFADevice で削除する必要がある (G)インラインポリシー ( DeleteUserPolicy ) ユーザーに追加されているインラインポリシー。1ユーザーあたり複数作成できる (H)アタッチされている管理ポリシー ( DetachUserPolicy ) アタッチされている管理ポリシー。1ユーザーあたり複数アタッチできる (I)IAMグループへの所属 ( RemoveUserFromGroup ) 1ユーザーあたり複数のグループに所属できる なんと9種類もあります。(C)(D)(E)あたりは使われるケースが少ないと思いますが、他はいずれも馴染み深いものですね。先にこれらのアイテムを削除しないと、 DeleteUser の呼び出しは失敗してしまいます。 コンソールからIAMユーザーを作成するときに起こっていること IAMユーザーと9つのアイテムとの関連付けについての理解を深めるために、マネジメントコンソールからIAMユーザーを作成する時に、裏側でどのような API が呼ばれているのかを掘り下げてみます。コンソールでユーザーを作る時にはいろいろなオプションがありますが、us-east-1 リージョンの CloudTrail ログより実際に呼び出された API をそれぞれ調べました。参照系の API も多く呼ばれていますが、更新系 API のみに着目します。 CreateUser 最初に必ず CreateUser API が呼ばれます。コンソールからユーザーを作成するときはパスワードかアクセスキーのどちらかを同時に作らなければなりませんが、実はこの API 単体ではいずれも生成されません。この時点ではコンソールアクセスもプログラムアクセスもできないユーザー本体のみができあがります。 これ以降の API コールはコンソールで選択した内容によって変わります。いずれも独立したリソース作成のため、実行順は重要ではありません。 (A)CreateLoginProfile 認証情報として「パスワード - AWS マネジメントコンソールへのアクセス」を選択しているとこの API が呼ばれ、パスワードが設定されます。 (B)CreateAccessKey 認証情報として「アクセスキー - プログラムによるアクセス」を選択しているとこの API が呼ばれ、アクセスキーが発行されます。 (G)PutUserPolicy アクセス許可の設定で「アクセス権限を既存のユーザーからコピー」でコピー元のユーザーにインラインポリシーがあるとこの API が呼ばれ、同じインラインポリシーが追加されます。複数のポリシーを追加可能です。 (H)AttachUserPolicy アクセス許可の設定で「既存のポリシーを直接アタッチ」する、もしくは「アクセス権限を既存のユーザーからコピー」でコピー元のユーザーに管理ポリシーがアタッチされているとこの API が呼ばれ、ユーザーに管理ポリシーがアタッチされます。複数のポリシーをアタッチ可能です。 (I)AddUserToGroup アクセス許可の設定で「ユーザーをグループに追加」する、もしくは「アクセス権限を既存のユーザーからコピー」でコピー元のユーザーがIAMグループに所属しているとこの API が呼ばれ、ユーザーがグループに所属します。複数のグループに所属可能です。 今のところ、「(C)署 名証 明書」「(D) SSH 公開鍵」「(E)各サービス用のクレデンシャル」「(F)MFAデ バイス 」はコンソールからのユーザー作成時に同時には作成できませんでした。 コンソールからIAMユーザーを削除するときに起こっていること 次に、(A)~(I)を全て追加したもりもりのユーザーを作成しておき、コンソールから削除した時にどのような API が裏側で呼ばれているのかを見てみました。更新系 API と、それに関連する参照系 API について記載します。 (A)DeleteLoginProfile パスワードが削除されます。 (B)ListAccessKeys > DeleteAccessKey まずは ListAccessKeys で削除対象ユーザーのアクセスキー一覧を取得し、見つかればそれぞれについて DeleteAccessKey で削除されます。 (C)ListSigningCertificates > DeleteSigningCertificate ListSigningCertificates で証明書一覧を取得し、見つかればそれぞれについて DeleteSigningCertificate で削除されます。 (D)ListSSHPublicKeys > DeleteSSHPublicKey ListSSHPublicKeys で鍵一覧を取得し、見つかればそれぞれについて DeleteSSHPublicKey で削除されます。 (E)ListServiceSpecificCredentials > DeleteServiceSpecificCredential ListServiceSpecificCredentials でクレデンシャル一覧を取得し、見つかればそれぞれについて DeleteServiceSpecificCredential で削除されます。 (F)ListMFADevices > DeactivateMFADevice > DeleteVirtualMFADevice ListMFADevices でデ バイス 一覧を取得し、見つかればそれぞれについて DeactivateMFADevice で無効化したのち、 DeleteVirtualMFADevice で削除されます。 (G)ListUserPolicies > DeleteUserPolicy ListUserPolicies でインラインポリシー一覧を取得し、見つかればそれぞれについて DeleteUserPolicy で削除されます。 (H)ListAttachedUserPolicies > DetachUserPolicy ListAttachedUserPolicies でアタッチされている管理ポリシー一覧を取得し、見つかればそれぞれについて DetachUserPolicy でデタッチされます。管理ポリシー自体は削除されません。 (I)ListGroupsForUser > RemoveUserFromGroup ListGroupsForUser で所属しているIAMグループ一覧を取得し、見つかればそれぞれについて RemoveUserFromGroup で所属が解除されます。IAMグループ自体は削除されません。 DeleteUser 以上のアイテムが全て削除されると、最後に DeleteUser が呼ばれてユーザー本体が晴れて(?)削除されます。画面では数クリックで終わる操作ですが、実は多くの API が呼ばれていることがわかりますね。 今回の仕組みでカバーする範囲 ユーザー削除時の API コールを参考にして、IAMユーザーを自動削除するための仕組みを作ります。以下ではCDKとLambda関数のコードを解説しますが、今回はIAMユーザー本体の削除に先立って、以下の5つのアイテムの削除のみを行います。 (A)ログインプロファイル (B)アクセスキー (G)インラインポリシー (H)アタッチされている管理ポリシー (I)IAMグループへの所属 (C)(D)(E)(F)についてはコンソールでユーザーを作成する時に同時に作成できないため、今回は省略しています。コンソールでも CLI でも、これらのアイテムを作成するためにはユーザー作成とは別の操作が必要になりますので、その操作が実施される前にEventBridgeイベントが発火し、ユーザーが削除されることが期待できます。もちろん、 CLI などから CreateUser 実行後、EventBridgeイベントが発火する前に(C)(D)(E)(F)を作成する API が素早く実行されるとユーザーの削除に失敗しますので、より確実にユーザーを削除したい場合は(C)(D)(E)(F)の削除も対応した方が良いでしょう。 CDKテンプレート IAMユーザーの作成を検知し、自動削除する仕組みのCDKスタックは次のとおりです。 EventBridgeルールは detailType を AWS API Call via CloudTrail 、 eventName を CreateUser にすることで、ユーザー作成 API の呼び出しがCloudTrailに記録されるとイベントが起動します。 前述の通り、IAMの更新系 API は us-east-1 リージョンのCloudTrailに記録されるので、このスタックも us-east-1 リージョンにデプロイする必要があります。 import { Duration , Stack , StackProps } from "aws-cdk-lib" ; import * as events from "aws-cdk-lib/aws-events" ; import * as eventTargets from "aws-cdk-lib/aws-events-targets" ; import * as iam from "aws-cdk-lib/aws-iam" ; import { Runtime } from "aws-cdk-lib/aws-lambda" ; import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs" ; import { Construct } from "constructs" ; export class DeleteUserStack extends Stack { constructor( scope: Construct , id: string , props?: StackProps ) { super( scope , id , props ); // Lambda関数 const deleteUserFunction = new lambdaNodejs.NodejsFunction ( this , "DeleteUserFunction" , { entry: "functions/delete-user.ts" , runtime: Runtime.NODEJS_16_X , timeout: Duration.minutes ( 15 ), } ); // 必要なActionを追加 deleteUserFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ "*" ] , actions: [ "iam:DeleteUser" , "iam:ListAccessKeys" , "iam:DeleteAccessKey" , "iam:ListGroupsForUser" , "iam:RemoveUserFromGroup" , "iam:GetLoginProfile" , "iam:DeleteLoginProfile" , "iam:ListAttachedUserPolicies" , "iam:DetachUserPolicy" , "iam:ListUserPolicies" , "iam:DeleteUserPolicy" , ] , effect: iam.Effect.ALLOW , } ) ); // EventBrdigeルール new events.Rule ( this , "CreateUserEventRule" , { ruleName: "create-user" , eventPattern: { source: [ "aws.iam" ] , detailType: [ "AWS API Call via CloudTrail" ] , detail: { eventSource: [ "iam.amazonaws.com" ] , eventName: [ "CreateUser" ] , } , } , targets: [ new eventTargets.LambdaFunction ( deleteUserFunction ) ] , } ); } } Lambda関数 functions/delete-user.ts のLambda関数の実装を説明します。 パッケージからのインポートとIAMClientの生成 IAM用の SDK をインポートします。Lambda関数が受け取るのはEventBridgeイベントなので、 aws-lambda パッケージから EventBridgeEvent もインポートします。 IAMClientは us-east-1 リージョン向けに生成します。 import * as iam from "@aws-sdk/client-iam" ; import { Handler , EventBridgeEvent } from "aws-lambda" ; const client = new iam.IAMClient ( { region: "us-east-1" } ); (A)ログインプロファイルの削除 ログインプロファイルを削除する関数です。最初に GetLoginProfileCommand でログインプロファイルが存在するかどうかを確認し、存在すれば while ループで削除を試みます。while ループにしている理由は、プロファイルの作成には時間がかかるようで、 CreateLoginProfile コール直後に DeleteLoginProfile をしても次のエラーになってしまうためです。 Login Profile for User <ユーザー名> cannot be modified while login profile is being created. そこで while ループで間に5秒間ずつ待機し、最大30回削除を試みる実装にしました。実際にやってみると、体感的にログインプロファイル作成後15秒ほどで削除できる状態になっていました。 export const deleteLoginProfile = async ( userName: string ) : Promise < void > => { try { const { LoginProfile } = await client.send (new iam.GetLoginProfileCommand ( { UserName: userName } )); if ( LoginProfile === undefined || LoginProfile.UserName === undefined ) return; } catch ( e: unknown ) { console .log ( ` ${ e instanceof Error ? e.message : "" } ` ); return; } let attempts = 30 ; while ( attempts > 0 ) { try { await client.send (new iam.DeleteLoginProfileCommand ( { UserName: userName } )); break; } catch ( e: unknown ) { console .log ( ` ${ e instanceof Error ? e.message : "" } ` ); } await new Promise (( resolve ) => setTimeout ( resolve , 5000 )); attempts --; } } ; (B)アクセスキーの削除 次の関数でアクセスキーを削除します。まずは ListAccessKeysCommand でアクセスキー一覧を取得します。アクセスキーの件数が多い場合(実際には各ユーザー2つまでなので起こらないと思いますが)、一回の ListAccessKeysCommand 呼び出しで全件取得できず、結果の IsTruncated プロパティが true で返ってきます。この時の Marker プロパティを次の ListAccessKeysCommand 呼び出しのパラメータに含めることで、続きの結果を取得できます。一覧取得後は DeleteAccessKeyCommand で1つずつ削除します。 export const deleteAccessKeys = async ( userName: string ) : Promise < void > => { let accessKeyIds: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListAccessKeysCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListAccessKeysCommand ( params )); if ( output.AccessKeyMetadata ) { accessKeyIds = accessKeyIds.concat ( output.AccessKeyMetadata.filter (( accessKeyMetadata ) => accessKeyMetadata.AccessKeyId ) .map ( ( accessKeyMetadata ) => accessKeyMetadata.AccessKeyId ! ) ); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( accessKeyIds.length === 0 ) return; await Promise . all( accessKeyIds.map (async ( keyId ) => { await client.send (new iam.DeleteAccessKeyCommand ( { UserName: userName , AccessKeyId: keyId } )); } ) ); } ; (G)インラインポリシーの削除 (B)アクセスキーの削除 と同様の処理なので、説明は割愛します。 export const deleteUserPolicies = async ( userName: string ) : Promise < void > => { let policyNames: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListUserPoliciesCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListUserPoliciesCommand ( params )); if ( output.PolicyNames ) { policyNames = policyNames.concat ( output.PolicyNames.filter (( name ) => name )); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( policyNames.length === 0 ) return; await Promise . all( policyNames.map (async ( name ) => { await client.send (new iam.DeleteUserPolicyCommand ( { UserName: userName , PolicyName: name } )); } ) ); } ; (H)管理ポリシーのデタッチ 同様の処理なので、説明は割愛します。 export const detachUserPolicies = async ( userName: string ) : Promise < void > => { let attachedPolicieArns: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListAttachedUserPoliciesCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListAttachedUserPoliciesCommand ( params )); if ( output.AttachedPolicies ) { attachedPolicieArns = attachedPolicieArns.concat ( output.AttachedPolicies.filter (( policy ) => policy.PolicyArn ) .map (( policy ) => policy.PolicyArn ! ) ); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( attachedPolicieArns.length === 0 ) return; await Promise . all( attachedPolicieArns.map (async ( arn ) => { await client.send (new iam.DetachUserPolicyCommand ( { UserName: userName , PolicyArn: arn } )); } ) ); } ; (I)IAMグループへの所属解除 同様の処理なので、説明は割愛します。 export const removeUserFromGroups = async ( userName: string ) : Promise < void > => { let groupNames: string [] = [] ; let shouldListNext = true ; let Marker: string | undefined = undefined ; while ( shouldListNext ) { const params: iam.ListGroupsForUserCommandInput = { UserName: userName , Marker } ; const output = await client.send (new iam.ListGroupsForUserCommand ( params )); if ( output.Groups ) { groupNames = groupNames.concat ( output.Groups.filter (( group ) => group.GroupName ) .map (( group ) => group.GroupName ! )); } output.IsTruncated ? ( Marker = output.Marker ) : ( shouldListNext = false ); } if ( groupNames.length === 0 ) return; await Promise . all( groupNames.map (async ( groupName ) => { await client.send (new iam.RemoveUserFromGroupCommand ( { UserName: userName , GroupName: groupName } )); } ) ); } ; Lambdaハンドラ 作成されたIAMユーザー名をイベントの中身から取得するための型を定義し、上で説明した関数を利用してLambdaハンドラを実装します。各アイテムの削除後に DeleteUserCommand でユーザー本体を削除します。 type Detail = { responseElements: { user: { userName: string } } } ; export const handler: Handler = async ( event: EventBridgeEvent < string , Detail >) => { const userName = event.detail.responseElements.user.userName ; await Promise . all( [ deleteLoginProfile ( userName ), deleteAccessKeys ( userName ), deleteUserPolicies ( userName ), detachUserPolicies ( userName ), removeUserFromGroups ( userName ), ] ); await client.send (new iam.DeleteUserCommand ( { UserName: userName } )); } ; これで、IAMユーザーが作成されるイベントをトリガーに、即座にユーザーを削除する仕組みが完成しました。 さいごに 自前で自動化の仕組みを実装してみると、普段コンソールでしている操作の裏側でどのような API が動いているのかが分かり、とても面白かったです。 最後までお読みただいてありがとうございました。 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 - セキュリティエンジニア(セキュリティ設計) 執筆: @kou.kinyo2 、レビュー: @higa ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、A as Bの呪文による画像合成の呪文です。 やまかずさんの 日刊 画像生成AI (2022年9月29日) の記事で紹介されていた 「A as B」は有効 を今回は検証してみました。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 A as Bの呪文とは まとめ 仲間募集 Stable Diffusionの過去コンテンツ A as Bの呪文とは A as Bの呪文は、AをBとして描画するというものです。Aの画像にBの画像が合成されたような効果が出ます。 例えば、次のような beautiful girl as cat の呪文を試してみましょう。 今回の呪文(横長、コピー&ペースト用) illustration of beautiful girl as cat detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) illustration of beautiful girl as cat detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 長い呪文は切り捨てられる編 参照 42 ['illustration</w>', 'of</w>', 'beautiful</w>', 'girl</w>', 'as</w>', 'cat</w>', 'detailed</w>', 'beautiful</w>', 'face</w>', 'detailed</w>', 'hair</w>', 'detailed</w>', 'perfect</w>', 'pupil</w>', 'of</w>', 'eyes</w>', 'detailed</w>', 'mouth</w>', 'detailed</w>', 'shoulders</w>', 'detailed</w>', 'bust</w>', 'looking</w>', 'far</w>', 'away</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 実は、この結果は、50回試して一回出るくらいの奇跡的な画像です。Aを人間、Bを動物にした場合、動物が耳に特徴があると、3, 4回に一回くらい、耳だけが人間に取り込まれます。 Bが犬など、耳にそれほど特徴がない場合、BがAに取り込まれる可能性がかなり減ります。 まとめ A as Bは、再現性が低いですが、うまくいくと面白い画像が作成できると言ったところでしょうか。 次回は、 かわいい動物の擬人化編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、A as Bの呪文による画像合成の呪文です。 やまかずさんの 日刊 画像生成AI (2022年9月29日) の記事で紹介されていた 「A as B」は有効 を今回は検証してみました。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 A as Bの呪文とは まとめ 仲間募集 Stable Diffusionの過去コンテンツ A as Bの呪文とは A as Bの呪文は、AをBとして描画するというものです。Aの画像にBの画像が合成されたような効果が出ます。 例えば、次のような beautiful girl as cat の呪文を試してみましょう。 今回の呪文(横長、コピー&ペースト用) illustration of beautiful girl as cat detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) illustration of beautiful girl as cat detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 長い呪文は切り捨てられる編 参照 42 ['illustration</w>', 'of</w>', 'beautiful</w>', 'girl</w>', 'as</w>', 'cat</w>', 'detailed</w>', 'beautiful</w>', 'face</w>', 'detailed</w>', 'hair</w>', 'detailed</w>', 'perfect</w>', 'pupil</w>', 'of</w>', 'eyes</w>', 'detailed</w>', 'mouth</w>', 'detailed</w>', 'shoulders</w>', 'detailed</w>', 'bust</w>', 'looking</w>', 'far</w>', 'away</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 実は、この結果は、50回試して一回出るくらいの奇跡的な画像です。Aを人間、Bを動物にした場合、動物が耳に特徴があると、3, 4回に一回くらい、耳だけが人間に取り込まれます。 Bが犬など、耳にそれほど特徴がない場合、BがAに取り込まれる可能性がかなり減ります。 まとめ A as Bは、再現性が低いですが、うまくいくと面白い画像が作成できると言ったところでしょうか。 次回は、 かわいい動物の擬人化編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、 蒸気機関 が高度に発達したレトロなアニメ( スチームパンク )の世界観編です。 Steampunk( スチームパンク )って言葉を知ってますか。SFから派生していて、 蒸気機関 (Steam)が高度に発達したレトロな世界観です。有名な作品だと「 天空の城ラピュタ 」、「 ハウルの動く城 」、「 鋼の錬金術師 」などがあります。 pixiv百科事典 スチームパンク 今回は、Steampunkの世界に飛び込んでみましょう。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting clockwork machines machines with cogs flying above clouds illuminated fountain in town まとめ 仲間募集 Stable Diffusionの過去コンテンツ steampunk illustration of huge battleship flying over city highly detailed 今回もシンプルな呪文から始めて、徐々に呪文を足していきましょう。 Steampunkなイラストを描くには、steampunk illustration ofで始めます。 今回の描画対象は、街の上を飛んでいる巨大な戦艦(huge battleship flying over city)です。 長い呪文は切り捨てられる編 で、説明したようにaやtheは必要ないので、省いています。 highly detailedを足すと描画対象のクオリティが上がります。 今回の呪文 steampunk illustration of huge battleship flying over city highly detailed トーク ン出力結果(改行版) 長い呪文は切り捨てられる編 参照 10 ['steampunk</w>', 'illustration</w>', 'of</w>', 'huge</w>', 'battleship</w>', 'flying</w>', 'over</w>', 'city</w>', 'highly</w>', 'detailed</w>'] 画像出力結果 artstation deviantart concept art digital painting award winning 作風用の呪文を追加しましょう。 artstation、 deviantart でアートの投稿サイトを指定します。 concept art、digital painting、award winningはクオリティを上げるためのお約束の呪文。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning 閲覧用呪文(改行版) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning トーク ン出力結果(改行版) 19 ['steampunk</w>', 'illustration</w>', 'of</w>', 'huge</w>', 'battleship</w>', 'flying</w>', 'over</w>', 'city</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>'] artstationがartとstationの2つの トーク ンに分かれてしまってますが、artstationが有効な呪文であることは、何度もあり/なしで確認しています。 画像出力結果 fantasy scene fantasy composition fantasy lighting 演出用の呪文を追加しましょう。scene(シーン)、composition(構図)、lighting(ライティング)を指定します。いろいろ試しましたが、Steampunkの世界観には、fantasyがあっているようです。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 25 ['steampunk</w>', 'illustration</w>', 'of</w>', 'huge</w>', 'battleship</w>', 'flying</w>', 'over</w>', 'city</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 良い感じに仕上がりましたね。今度は、描画対象( サブジ ェクト)を変えてみましょう。 clockwork machines clockwork machines(ゼンマイ仕掛けの機械)を試してみましょう。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of clockwork machines highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of clockwork machines highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 22 ['steampunk</w>', 'illustration</w>', 'of</w>', 'clockwork</w>', 'machines</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 machines with cogs flying above clouds 今度は、雲の上を飛んでいる歯車付き機械です。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of machines with cogs flying above clouds highly detailed artstation deviantart concept art digital painting award winning fantasy scene golden hour fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of machines with cogs flying above clouds highly detailed artstation deviantart concept art digital painting award winning fantasy scene golden hour fantasy composition fantasy lighting トーク ン出力結果(改行版) 29 ['steampunk</w>', 'illustration</w>', 'of</w>', 'machines</w>', 'flying</w>', 'above</w>', 'clouds</w>', 'with</w>', 'co', 'gs</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'golden</w>', 'hour</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] cogs(歯車)がcoとgsに分割されてしまっていますが、cogsが有効な呪文であることは何度も試して確認済みです。 画像出力結果 illuminated fountain in town 最後は、街の中のライトアップされた噴水です。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of illuminated fountain in town highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of illuminated fountain in town highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 24 ['steampunk</w>', 'illustration</w>', 'of</w>', 'illuminated</w>', 'fountain</w>', 'in</w>', town</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 まとめ 今回は、Steampunkのイラストを扱いました。 蒸気機関 が高度に発達したレトロなアニメ( スチームパンク )の世界観、好きな人は多いのではないでしょうか。 次回は、 A as Bの呪文による画像合成編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、 蒸気機関 が高度に発達したレトロなアニメ( スチームパンク )の世界観編です。 Steampunk( スチームパンク )って言葉を知ってますか。SFから派生していて、 蒸気機関 (Steam)が高度に発達したレトロな世界観です。有名な作品だと「 天空の城ラピュタ 」、「 ハウルの動く城 」、「 鋼の錬金術師 」などがあります。 pixiv百科事典 スチームパンク 今回は、Steampunkの世界に飛び込んでみましょう。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting clockwork machines machines with cogs flying above clouds illuminated fountain in town まとめ 仲間募集 Stable Diffusionの過去コンテンツ steampunk illustration of huge battleship flying over city highly detailed 今回もシンプルな呪文から始めて、徐々に呪文を足していきましょう。 Steampunkなイラストを描くには、steampunk illustration ofで始めます。 今回の描画対象は、街の上を飛んでいる巨大な戦艦(huge battleship flying over city)です。 長い呪文は切り捨てられる編 で、説明したようにaやtheは必要ないので、省いています。 highly detailedを足すと描画対象のクオリティが上がります。 今回の呪文 steampunk illustration of huge battleship flying over city highly detailed トーク ン出力結果(改行版) 長い呪文は切り捨てられる編 参照 10 ['steampunk</w>', 'illustration</w>', 'of</w>', 'huge</w>', 'battleship</w>', 'flying</w>', 'over</w>', 'city</w>', 'highly</w>', 'detailed</w>'] 画像出力結果 artstation deviantart concept art digital painting award winning 作風用の呪文を追加しましょう。 artstation、 deviantart でアートの投稿サイトを指定します。 concept art、digital painting、award winningはクオリティを上げるためのお約束の呪文。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning 閲覧用呪文(改行版) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning トーク ン出力結果(改行版) 19 ['steampunk</w>', 'illustration</w>', 'of</w>', 'huge</w>', 'battleship</w>', 'flying</w>', 'over</w>', 'city</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>'] artstationがartとstationの2つの トーク ンに分かれてしまってますが、artstationが有効な呪文であることは、何度もあり/なしで確認しています。 画像出力結果 fantasy scene fantasy composition fantasy lighting 演出用の呪文を追加しましょう。scene(シーン)、composition(構図)、lighting(ライティング)を指定します。いろいろ試しましたが、Steampunkの世界観には、fantasyがあっているようです。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of huge battleship flying over city highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 25 ['steampunk</w>', 'illustration</w>', 'of</w>', 'huge</w>', 'battleship</w>', 'flying</w>', 'over</w>', 'city</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 良い感じに仕上がりましたね。今度は、描画対象( サブジ ェクト)を変えてみましょう。 clockwork machines clockwork machines(ゼンマイ仕掛けの機械)を試してみましょう。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of clockwork machines highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of clockwork machines highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 22 ['steampunk</w>', 'illustration</w>', 'of</w>', 'clockwork</w>', 'machines</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 machines with cogs flying above clouds 今度は、雲の上を飛んでいる歯車付き機械です。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of machines with cogs flying above clouds highly detailed artstation deviantart concept art digital painting award winning fantasy scene golden hour fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of machines with cogs flying above clouds highly detailed artstation deviantart concept art digital painting award winning fantasy scene golden hour fantasy composition fantasy lighting トーク ン出力結果(改行版) 29 ['steampunk</w>', 'illustration</w>', 'of</w>', 'machines</w>', 'flying</w>', 'above</w>', 'clouds</w>', 'with</w>', 'co', 'gs</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'golden</w>', 'hour</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] cogs(歯車)がcoとgsに分割されてしまっていますが、cogsが有効な呪文であることは何度も試して確認済みです。 画像出力結果 illuminated fountain in town 最後は、街の中のライトアップされた噴水です。 今回の呪文(横長、コピー&ペースト用) steampunk illustration of illuminated fountain in town highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting 閲覧用呪文(改行版) steampunk illustration of illuminated fountain in town highly detailed artstation deviantart concept art digital painting award winning fantasy scene fantasy composition fantasy lighting トーク ン出力結果(改行版) 24 ['steampunk</w>', 'illustration</w>', 'of</w>', 'illuminated</w>', 'fountain</w>', 'in</w>', town</w>', 'highly</w>', 'detailed</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'fantasy</w>', 'scene</w>', 'fantasy</w>', 'composition</w>', 'fantasy</w>', 'lighting</w>'] 画像出力結果 まとめ 今回は、Steampunkのイラストを扱いました。 蒸気機関 が高度に発達したレトロなアニメ( スチームパンク )の世界観、好きな人は多いのではないでしょうか。 次回は、 A as Bの呪文による画像合成編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )
はじめまして、X イノベーション 本部 オープン イノベーション ラボの飯田です。 ISIDでは、有志で論文輪読会を実施しています。 tech.isid.co.jp 今回、その中で、私が読んだDNAストレージについてご紹介します。 DNAストレージという技術は、DNAを情報記録媒体として用いる技術です。 DNAの 塩基配列 をうまく活用することで、デジタルデータの読み書きが可能となります。 マイクロソフト 等が積極的に研究を行っているようです ( Microsoft Research:DNA Storage ) 輪読会では、以下2本の論文を紹介しました。 その内容を簡単にご紹介します。 Bornholt, J. et al. (2016) A DNA-based Archival Storage System DNAストレージが初登場したと言われる Clelland C. T. et al. (1999) Hiding messages in DNA microdots 前提知識 なぜDNAストレージ? ビックデータ時代のストレージの課題とDNAストレージ DNAストレージの簡単な原理 最初のDNAストレージ DNAストレージの課題 (冗長性・エラー耐性 と ランダムアクセス) 冗長性・エラー耐性 ランダムアクセス Bornholt, J. et al. (2016) のアーキテクチャ 書き込み処理 の流れ 読み出し処理 の流れ 必要であれば、DNAストレージの増幅 実例:実際に保存されたデータ例 最後に、バイオロジー×IT技術の可能性! 前提知識 DNA ヒトの細胞では、核の中の染色体にあり、A(アデニン)・T(チミン)・G(グアニン)・C(シトシン)の4種で構成されている。 DNA中ではAとT、GとCが結合していて、その結合の対を塩基対と言います。 DNA合成技術 狙った 塩基配列 でDNAを生み出す方法。DNAストレージではWriteに相当する手技。 シークエンシング DNAの構成成分であるATGC 4種の 塩基配列 を決定すること。 古典的なサンガー法から、NGSまでいくつかの手法がある。DNAストレージではReadに相当する手技。 なぜDNAストレージ? ビックデータ時代のストレージの課題とDNAストレージ 現在のビックデータ時代、現状の 記憶メディア では保存密度や寿命が課題になると言われています。 磁気メディア:185 TB のテープ・カートリッジ(密度:10 GB/ mm3 程度) 光学メディア:1PBを格納できる光ディスクの実現可能性(密度:約100GB/ mm3 ) 回転ディスクの寿命:3~5年 テープの寿命:10~30年 その課題を解決する方法として、DNAストレージが注目されています。 バイオテク ノロ ジー の標準的な手法で実現可能 非常に高密度:理論的な限界値 1EB/ mm3 (テープの8倍) 長寿命:500年以上の 半減期 DNAベースの生命が存在する限り、DNAを読み取り、操作可能 DNAストレージによって、より高密度・長期保存が可能になると期待されています。 生物として生きていれば、生命が続く限りデータは保存される?ということになると思います。 DNAストレージの簡単な原理 ものすごく単 純化 すると、デジタルデータを0/1のビットで情報表現するものが、DNAストレージではそれをATGCの4種で表現するイメージになります。 大きな流れとしては、以下のとおりです。 デジタルデータをDNAの ヌクレオチド 配列に マッピング し、対応するDNA分子を合成し、保存する データの読み取りは、DNA分子の 塩基配列 を解読して、元のデジタルデータに戻す DNAストレージは読み取りに PCR やシークエンシングが必要となるため、超 アーカイブ 向けのストレージ(数時間から数日の アクセス時間 、高密度で耐久性)の特性があります。 具体例でみてみましょう。 最初のDNAストレージ 暗号表と 塩基配列 を マッピング して、「J U N E 6 I N V A S I O N : N O R M A N D Y」23文字をDNA上に保存し、読み取りにも成功したという報告です。 こちらの暗号表をもとに、英数字と 塩基配列 を対応させて、DNAに保存読み取りを実現しました。 DNAストレージの課題 (冗長性・エラー耐性 と ランダムアクセス) 冗長性・エラー耐性 DNAストレージでは、下記のようにDNAとしての物質に由来するエラーと劣化があります。 実用化をするためには、それに対応するための冗長性・エラー耐性の仕組みが必須となります。 DNAの合成と読み取りの不完全さ:DNA合成・シークエンシングではエラーが入ってしまう 塩基配列 は保存中に劣化する可能性 (データの完全性の欠損) ランダムアクセス DNAシークエンシングは、DNA全体に対して行う必要があるため、DNAストレージから1バイトでも読みだすには、DNA全体を解読(シークエンシング)する必要があります。 キーバリュー方式の格納法と PCR を組み合せ、必要なデータのみを増幅することで、ランダムアクセスを実現する方法が提案されています。 Bornholt, J. et al. (2016) の アーキテクチャ 最初の報告では、単純なものでしたが、ランダムアクセス等に対応した アーキテクチャ がこちらです。 書き込み処理 の流れ キーと値を入力 いわゆるKey- Value ストアのように保存するイメージになります キーは PCR プライマー配列、格納される場所 値はデータの値 DNA配列の生成 データアドレス、 ペイロード 、エラー検出コード等も符号化し、 PCR プライマーのターゲット配列を添付して、合成 できあがったDNAは、保存用ライブラリに格納 読み出し処理 の流れ 読み取りたいキーを入力し、物理的に抽出 キーの PCR プライマーを取得 保存されているデータを含むDNAプールからサンプルを物理的に抽出 (その中には無関係なデータも含まれている) PCR によりDNAを増幅し、シークエンシング キーに指定された PCR プライマーにより、目的のDNAだけが増幅される シークエンシングにより、デジタルデータの読み出し 必要であれば、DNAストレージの増幅 読み出し処理後、サンプルが減ってしまうため、必要に応じて PCR で複製を行う 実例:実際に保存されたデータ例 Bornholt (2016) では、 5kBから84kBまでの4つの画像ファイルを保存し、ランダムアクセスができることを確認しています 3つのファイルはエラーなしで復元できました。cat.jpgのみ、 JPEG ヘッダーに1バイトのエラーが発生しましたが、修正して参照できました。 最後に、バイオロ ジー × IT技術 の可能性! 論文の最後に下記の言葉がありました。 Given the impending limits of silicon technology, we believe that hybrid silicon and biochemical systems are worth serious consideration: time is ripe for computer architects to consider incorporating biomolecules as an integral part of computer design. now is the time for the computer industry to borrow back from the biotechnology industry to advance the state of the art in computer systems. ムーアの法則 に限界がきていると言われるように、個人的には、既存の IT技術 の延長では限界があると思っています。 バイオ技術など、他の業界の技術を大胆に取り入れてブレイクスルーを目指すのは夢があります。 量子コンピュータ は 量子力学 をベースとしているように、バイオコンピュータの登場も夢ではないと思います。 執筆: @iida.michitaka 、レビュー: @sato.taichi ( Shodo で執筆されました )
はじめまして、X イノベーション 本部 オープン イノベーション ラボの飯田です。 ISIDでは、有志で論文輪読会を実施しています。 tech.isid.co.jp 今回、その中で、私が読んだDNAストレージについてご紹介します。 DNAストレージという技術は、DNAを情報記録媒体として用いる技術です。 DNAの 塩基配列 をうまく活用することで、デジタルデータの読み書きが可能となります。 マイクロソフト 等が積極的に研究を行っているようです ( Microsoft Research:DNA Storage ) 輪読会では、以下2本の論文を紹介しました。 その内容を簡単にご紹介します。 Bornholt, J. et al. (2016) A DNA-based Archival Storage System DNAストレージが初登場したと言われる Clelland C. T. et al. (1999) Hiding messages in DNA microdots 前提知識 なぜDNAストレージ? ビックデータ時代のストレージの課題とDNAストレージ DNAストレージの簡単な原理 最初のDNAストレージ DNAストレージの課題 (冗長性・エラー耐性 と ランダムアクセス) 冗長性・エラー耐性 ランダムアクセス Bornholt, J. et al. (2016) のアーキテクチャ 書き込み処理 の流れ 読み出し処理 の流れ 必要であれば、DNAストレージの増幅 実例:実際に保存されたデータ例 最後に、バイオロジー×IT技術の可能性! 前提知識 DNA ヒトの細胞では、核の中の染色体にあり、A(アデニン)・T(チミン)・G(グアニン)・C(シトシン)の4種で構成されている。 DNA中ではAとT、GとCが結合していて、その結合の対を塩基対と言います。 DNA合成技術 狙った 塩基配列 でDNAを生み出す方法。DNAストレージではWriteに相当する手技。 シークエンシング DNAの構成成分であるATGC 4種の 塩基配列 を決定すること。 古典的なサンガー法から、NGSまでいくつかの手法がある。DNAストレージではReadに相当する手技。 なぜDNAストレージ? ビックデータ時代のストレージの課題とDNAストレージ 現在のビックデータ時代、現状の 記憶メディア では保存密度や寿命が課題になると言われています。 磁気メディア:185 TB のテープ・カートリッジ(密度:10 GB/ mm3 程度) 光学メディア:1PBを格納できる光ディスクの実現可能性(密度:約100GB/ mm3 ) 回転ディスクの寿命:3~5年 テープの寿命:10~30年 その課題を解決する方法として、DNAストレージが注目されています。 バイオテク ノロ ジー の標準的な手法で実現可能 非常に高密度:理論的な限界値 1EB/ mm3 (テープの8倍) 長寿命:500年以上の 半減期 DNAベースの生命が存在する限り、DNAを読み取り、操作可能 DNAストレージによって、より高密度・長期保存が可能になると期待されています。 生物として生きていれば、生命が続く限りデータは保存される?ということになると思います。 DNAストレージの簡単な原理 ものすごく単 純化 すると、デジタルデータを0/1のビットで情報表現するものが、DNAストレージではそれをATGCの4種で表現するイメージになります。 大きな流れとしては、以下のとおりです。 デジタルデータをDNAの ヌクレオチド 配列に マッピング し、対応するDNA分子を合成し、保存する データの読み取りは、DNA分子の 塩基配列 を解読して、元のデジタルデータに戻す DNAストレージは読み取りに PCR やシークエンシングが必要となるため、超 アーカイブ 向けのストレージ(数時間から数日の アクセス時間 、高密度で耐久性)の特性があります。 具体例でみてみましょう。 最初のDNAストレージ 暗号表と 塩基配列 を マッピング して、「J U N E 6 I N V A S I O N : N O R M A N D Y」23文字をDNA上に保存し、読み取りにも成功したという報告です。 こちらの暗号表をもとに、英数字と 塩基配列 を対応させて、DNAに保存読み取りを実現しました。 DNAストレージの課題 (冗長性・エラー耐性 と ランダムアクセス) 冗長性・エラー耐性 DNAストレージでは、下記のようにDNAとしての物質に由来するエラーと劣化があります。 実用化をするためには、それに対応するための冗長性・エラー耐性の仕組みが必須となります。 DNAの合成と読み取りの不完全さ:DNA合成・シークエンシングではエラーが入ってしまう 塩基配列 は保存中に劣化する可能性 (データの完全性の欠損) ランダムアクセス DNAシークエンシングは、DNA全体に対して行う必要があるため、DNAストレージから1バイトでも読みだすには、DNA全体を解読(シークエンシング)する必要があります。 キーバリュー方式の格納法と PCR を組み合せ、必要なデータのみを増幅することで、ランダムアクセスを実現する方法が提案されています。 Bornholt, J. et al. (2016) の アーキテクチャ 最初の報告では、単純なものでしたが、ランダムアクセス等に対応した アーキテクチャ がこちらです。 書き込み処理 の流れ キーと値を入力 いわゆるKey- Value ストアのように保存するイメージになります キーは PCR プライマー配列、格納される場所 値はデータの値 DNA配列の生成 データアドレス、 ペイロード 、エラー検出コード等も符号化し、 PCR プライマーのターゲット配列を添付して、合成 できあがったDNAは、保存用ライブラリに格納 読み出し処理 の流れ 読み取りたいキーを入力し、物理的に抽出 キーの PCR プライマーを取得 保存されているデータを含むDNAプールからサンプルを物理的に抽出 (その中には無関係なデータも含まれている) PCR によりDNAを増幅し、シークエンシング キーに指定された PCR プライマーにより、目的のDNAだけが増幅される シークエンシングにより、デジタルデータの読み出し 必要であれば、DNAストレージの増幅 読み出し処理後、サンプルが減ってしまうため、必要に応じて PCR で複製を行う 実例:実際に保存されたデータ例 Bornholt (2016) では、 5kBから84kBまでの4つの画像ファイルを保存し、ランダムアクセスができることを確認しています 3つのファイルはエラーなしで復元できました。cat.jpgのみ、 JPEG ヘッダーに1バイトのエラーが発生しましたが、修正して参照できました。 最後に、バイオロ ジー × IT技術 の可能性! 論文の最後に下記の言葉がありました。 Given the impending limits of silicon technology, we believe that hybrid silicon and biochemical systems are worth serious consideration: time is ripe for computer architects to consider incorporating biomolecules as an integral part of computer design. now is the time for the computer industry to borrow back from the biotechnology industry to advance the state of the art in computer systems. ムーアの法則 に限界がきていると言われるように、個人的には、既存の IT技術 の延長では限界があると思っています。 バイオ技術など、他の業界の技術を大胆に取り入れてブレイクスルーを目指すのは夢があります。 量子コンピュータ は 量子力学 をベースとしているように、バイオコンピュータの登場も夢ではないと思います。 執筆: @iida.michitaka 、レビュー: @sato.taichi ( Shodo で執筆されました )
みなさんこんにちは、 電通国際情報サービス (ISID)コーポレート本部 システム推進部の佐藤太一です。 このエントリでは Google Dataflowを使ったデータ分析パイプライン構築において中心的な API の使い方について説明します。 Google Dataflowとはなにか Dataflowの開発環境構築 GradleによるDataflowプロジェクトの作り方 Apache Beamの基礎 Pipelineについて PCollectionについて ParDoを使った逐次処理の書き方 Dataflowによるユニットテストの書き方 フィルター フィルターのテスト 値の増幅処理 増幅処理のテスト PCollectionの分岐 PCollectionの分岐をテストする まとめ Google Dataflowとはなにか Google DataflowはいわゆるExtract/Transform/Load(ETL)ツールの一種です。 Apache Beam という バッチ処理 基盤を GCP の分散処理環境で動かしてくれます。 Apache Beam自体は、 Apache Flink や Apache Spark 、 Hazelcast Jet といったオンプレミスで動作する実行環境を利用することもできます。 Apache Beamでは、 Java や Python 、Goといった言語で処理を記述できますが、今回は Java を使って説明します。 Dataflowの開発環境構築 まずは、Dataflowの開発環境を作っていきましょう。 開発環境として使うマシンには、事前にJava17とGradle7.5以上をインストールしておいてください。 GradleによるDataflowプロジェクトの作り方 最初にプロジェクト全体を格納するための ディレクト リを作成しましょう。 ここからは、この記事内でシェルコマンドを実行するよう説明している部分では、必ずこのルート ディレクト リで実行してください。 作るプロジェクトは、説明のために dataflow-example とします。作った dataflow-example ディレクト リの中で、以下のコマンドを実行して最小限のプロジェクトを作成します。 gradle init --type basic --dsl kotlin --project-name dataflow-example --incubating 最小限とはいえ、Gradle Wrapperとなる シェルスクリプト やgit用の設定ファイルが生成されていますね。 この中から、 build.gradle.kts を以下のように編集します。 plugins { id("java") } group = "com.example.dataflow" version = "0.1.0-SNAPSHOT" java.toolchain { languageVersion.set(JavaLanguageVersion.of(17)) // 1. } repositories { mavenCentral() maven("https://packages.confluent.io/maven/") // 2. } dependencies { var beamVersion = "2.41.0" // 3. var slf4jVersion = "1.7.36" implementation(platform("com.google.cloud:libraries-bom:25.4.0")) // 4. implementation("org.apache.beam:beam-sdks-java-core:${beamVersion}") // 5. implementation("org.apache.beam:beam-sdks-java-io-google-cloud-platform:${beamVersion}") // 5. implementation("org.apache.beam:beam-runners-google-cloud-dataflow-java:${beamVersion}") // 5. implementation("org.apache.commons:commons-csv:1.9.0") implementation("org.slf4j:slf4j-api:${slf4jVersion}") implementation("org.slf4j:slf4j-jdk14:${slf4jVersion}") testImplementation("junit:junit:4.13.2") // 6. testImplementation("org.apache.beam:beam-runners-direct-java:${beamVersion}") // 7. } tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" } このビルド スクリプト で利用する コンパイラ やランタイムのバージョン番号を指定しています。 Gradleを実行している Java ランタイムのバージョンが開発者ごとにズレていても、 コンパイル やテストに使う Java ランタイムは統一できるということです。ビルドの再現性が高まりますので必ず設定しましょう。 この機能を使うと、必要に応じてGradleがビルド済みの JDK を自動的にダウンロードしてくれます。 デフォルトでは Adoptium を使います。 依存ライブラリをダウンロードする先を宣言しています。 最初に指定しているのは Maven のデフォルト アーティファクト リポジトリ である Maven Central Repository です。 二つ目に指定しているのはconfluentが公開している リポジトリ です。これはKafka関連のライブラリで、この リポジトリ 内にしかないものがあるからです。 Apache Beamのバージョンを参照する依存性がいくつかあるので、ここでは変数として切り出しています。 Dataflowを動かすために必要な GCP の SDK に対する依存性を宣言しています。 GradleのPlatform機能 を使っていますね。 Apache Beamに対する依存性を宣言しています。後半の二つは GCP でBeamを動かすために必要な依存性です。 ユニットテスト 用の依存性としてJUnit4を指定しています。 JUnit の最新版はJUnit5系ですが、記事執筆時点において Apache BeamはJUnit5をサポートしていません。cf. JUnit5 support Apache Beam用の ユニットテスト ライブラリに対する依存性を宣言しています。 これでDataflow用のローカルビルド環境の構築は完了です。 Apache Beamの基礎 Apache Beamを理解するなら、まずはPipelineとPCollectionをしっかり理解してください。 他のコンセプトについては、Dataflowのドキュメントを参照してください。 * Apache Beam のプログラミング モデル Pipelineについて Pipelineは複数のステップから構成される処理全体を表すオブジェクトで、データの読み取り処理から始まりフィルターや変換を経て、出力処理までを行います。一つのパイプラインが一つのジョブとなります。 Pipelineを構成する各ステップは、実行環境が必要に応じて分散処理してくれます。つまり、各ステップを効率よく動作させるには、それぞれのステップが全く違ったプロセスの上で非同期に実行されても問題がおきないようにしましょう。 具体的には、処理単位になるデータの独立性をできるかぎり高めるようにします。つまり、 RDB における非正規化を積極的に行うようなデータの持ち方をします。 順序に強い整合性を求める書き方もできますが、そうすると分散処理環境がもつ性能を十分に引き出せません。 PCollectionについて PCollectionはPipelineを流れるデータの集合を表すオブジェクトです。 逐次的に要素を扱えるので Java のCollection Frameworkと似ていますが、PCollectionの API だけではデータの開始と終了を明示的に調べられません。 また、PCollectionに格納されている要素は、分散処理環境内における実行環境の都合で シリアライズ されたりコピーされる可能性があります。つまり、処理に必要な情報は全て要素内に内包する必要があります。 ParDoを使った逐次処理の書き方 基本的な概念が分かった所で本題に入っていきましょう。 最初に作るのは、PCollectionを流れる要素を1:1で変換していく処理です。 この図は四角い枠がPCollectionで、〇が各要素、矢印が処理です。つまり、全体がPipelineとなります。 今回の記事では、全てのコードをテストコードとして実装しますので、以下のように ディレクト リを作成します。 mkdir src/test/java/com/example/dataflow 出来た ディレクト リに CsvFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; import java.util.*; public class CsvFn extends DoFn<String, List<String>> { // 1. @ProcessElement // 2. public void processElement(ProcessContext c) throws Exception { // 3. var element = c.element(); // 4. var list = Arrays.asList(element.split(",")); // 5. c.output(list); // 6. } } この処理では、単一の文字列を入力すると、それをカンマ区切りで分割したリストとして後続の処理に引き渡します。 逐次処理を実装する際に使うクラスは DoFn を継承します。 一つ目の型パラメータは、各入力要素を表す型を設定します。ここでは String を設定しています。 二つ目の型パラメータは、各出力要素を表す型を設定します。ここでは List<String> を設定しています。 逐次処理を行うメソッドは @ProcessElement アノテーション を付与します。 逐次処理を行うメソッドのアクセス修飾子は public 、戻り値は void です。送出される例外としては Exception を定義しておきます。 なお、このメソッドの中から例外を送出するとジョブ全体が停止します。 ProcessContext の element メソッドを呼ぶと、 DoFn を継承する際に設定した一番目の型パラメータの変数が得られます。ここでは String 型の変数が得られるわけです。 String の split メソッドを呼びだして得られた配列を List に格納しています。 ProcessContext の output メソッドを呼ぶ際には、 DoFn を継承する際に設定した二番目の型パラメータの変数を渡します。ここでは既に作成済みの list を渡していますね。 Dataflowによる ユニットテスト の書き方 次は、逐次処理を ユニットテスト してみましょう。 CsvFn.java と同じ ディレクト リ内に CsvFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class CsvFnTest { static final List<String> values = List.of( "foo,bar,baz", "fo1,ba2,ba3", "ba1,ba2,ba3"); @Rule public TestPipeline pipeline = TestPipeline.create(); // 1. @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) // 2. .apply(ParDo.of(new CsvFn())); // 3. PAssert.that(output).containsInAnyOrder( // 4. List.of("foo", "bar", "baz"), List.of("ba1", "ba2", "ba3"), // 5. List.of("fo1", "ba2", "ba3") ); pipeline.run().waitUntilFinish(); // 6. } } ユニットテスト 用のパイプラインを生成しています。パイプラインを構成するための共通処理があるので @Rule を付与しています。 Create の of メソッドを使って文字列のリストをパイプラインに流せる形に変換しています。ここでは、 List の各要素がパイプラインを流れていきます。 ParDo の of メソッドに先ほど実装した CsvFn を インスタンス 化して渡しています。これによって、パイプラインを流れる各要素ごとに CsvFn の processElement メソッドが呼びだされます。 パイプラインを流れる要素が正しく変換されているか確認するには、 Apache Beamで用意されている専用の PAssert を使います。ここでは containsInAnyOrder メソッドを使ってそれぞれの要素が正しくカンマ区切りで分解されたか確認しています。 values 変数として定義した要素の順序とは違った順序で要素を検証しています。これは、パイプラインを流れる要素の処理順序は保証されておらず、実行環境の都合で任意に入れ替わる可能性があることを意図しています。つまり、 Create の of メソッドで作った要素がそのままの順序で CsvFn の processElement メソッドに入ってくるとは限りません。 TestPipeline の run メソッドを呼びだした上で、さらに waitUntilFinish メソッドを呼んでパイプラインの処理が終わるのを待っています。デバッガで実行する際に注意してほしいのは、この時点で初めてパイプラインの処理が動き始めることです。つまり、 4. の時点では、まだ CsvFn の processElement メソッドは呼びだされません。 フィルター CsvFnでは単純な1:1の変換処理を実装しましたので、次はフィルター処理を実装してみましょう。 フィルター処理として作るのは、指定した長さよりも長い文字列だけを後続の処理に流すフィルターです。 CsvFn.java と同じ ディレクト リ内に FilterFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; public class FilterFn extends DoFn<String, String> { // 1. final int size; // 2. public FilterFn(int size) { this.size = size; } @ProcessElement public void processElement(ProcessContext c) { var element = c.element(); if (size < element.length()) { // 3. c.output(element); } } } ここで実装するのはフィルター処理なので、入力と出力の型は同じです。 コンスト ラク タで受け取った長さをメンバ変数として格納しています。 Apache BeamではPCollectionの要素だけでなく、各処理のステップを表すオブジェクトも実行環境の都合で直列化される可能性があります。つまり、メンバ変数としてはSerializableな型(もしくは、Externalizableな型)だけを定義できます。 条件分岐に基づいて ProcessContext の output メソッドを呼びだすかどうかを決めています。 フィルターのテスト では、フィルター処理を ユニットテスト してみましょう。 CsvFn.java と同じ ディレクト リ内に FilterFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class FilterFnTest { static final List<String> values = List.of( "alpha", "beta", "gamma"); @Rule public TestPipeline pipeline = TestPipeline.create(); @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) .apply(ParDo.of(new FilterFn(4))); PAssert.that(output).containsInAnyOrder("gamma", "alpha"); pipeline.run().waitUntilFinish(); } } 長さが4文字より大きい単語をフィルターできていますね。 値の増幅処理 次は、一つの入力から複数回の出力を行う処理を実装してみましょう。 増幅処理として作るのは、文字列をカンマ区切りで分割した各要素をそのまま後続に渡す処理です。 CsvFn.java と同じ ディレクト リ内に FlatValuesFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; import java.util.Arrays; public class FlatValuesFn extends DoFn<String, String> { // 1. @ProcessElement public void processElement(ProcessContext c) { var element = c.element(); var list = Arrays.asList(element.split(",")); list.forEach(c::output); // 2. } } ここで実装するのは増幅処理なので、入力と出力の型は同じです。 文字列を分割して得られた要素全てについて ProcessContext の ouput メソッドを呼びだしています。 増幅処理のテスト では、増幅処理を ユニットテスト してみましょう。 CsvFn.java と同じ ディレクト リ内に FlatValuesFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class FlatValuesFnTest { static final List<String> values = List.of( "foo,bar,baz", "fo1,ba2,ba3", "ba1,ba2,ba3"); @Rule public TestPipeline pipeline = TestPipeline.create(); @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) .apply(ParDo.of(new FlatValuesFn())); PAssert.that(output).containsInAnyOrder( "foo", "bar", "baz", "ba1", "ba2", "ba3", "fo1", "ba2", "ba3" ); pipeline.run().waitUntilFinish(); } } カンマ区切りで3つずつに分割できる要素を3回 FlatValuesFn で処理したので9つの要素が出力されていますね。 PCollectionの分岐 ここまでの処理では、処理の流れであるPCollection自体は1つのまま要素が流れていきました。 しかし、データの1カラム目だけを見て後続の処理を切り替えるといった処理構造を実現したくなることはあります。 ここでは、入力された文字列の1文字目を使って後続の処理を切り替えるためにPCollectionを分岐してみましょう。 CsvFn.java と同じ ディレクト リ内に BranchFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.values.TupleTag; import java.util.*; public class BranchFn extends DoFn<String, List<String>> { static final TupleTag<List<String>> MAIN = new TupleTag<>() { // 1. }; static final TupleTag<List<String>> SUB = new TupleTag<>() { }; @ProcessElement public void processElement(ProcessContext c) { var element = c.element(); var list = Arrays.asList(element.split(",")); if (list.get(0).equals("M")) { c.output(MAIN, list.subList(1, list.size()));       // 2. } else { c.output(SUB, list.subList(1, list.size())); } } } TupleTag は実行環境全体で一意のIDを付与する必要があります。ここでは、ややトリッキーなテクニックを使ってそれを実現しています。コンスト ラク タ呼び出しの後ろについている中括弧 {} によってインナークラスを作成していることがポイントです。実装の詳細が気になる方は是非コードを読んでみてください。 ProcessContext の ouput メソッドを呼びだす際に、 TupleTag を渡しています。これによって各要素にタグ付けをすることで、PCollectionの分岐を実現しているのです。 PCollectionの分岐をテストする では、分岐したPCollectionをどのように扱うのかテストコードで確認してみましょう。 CsvFn.java と同じ ディレクト リ内に BranchFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.apache.beam.sdk.values.TupleTagList; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class BranchFnTest { static final List<String> values = List.of( "M,foo,bar,baz", "S,fo1,ba2,ba3", "M,ba1,ba2,ba3"); // 1. @Rule public TestPipeline pipeline = TestPipeline.create(); @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) .apply(ParDo.of(new BranchFn()) .withOutputTags(BranchFn.MAIN, TupleTagList.of(List.of(BranchFn.SUB))) // 2. ); PAssert.that(output.get(BranchFn.MAIN)).containsInAnyOrder( // 3. List.of("foo", "bar", "baz"), List.of("ba1", "ba2", "ba3") ); PAssert.that(output.get(BranchFn.SUB)).containsInAnyOrder( // 4. List.of("fo1", "ba2", "ba3") ); pipeline.run().waitUntilFinish(); } } テストデータとして、各要素の先頭に分岐の条件となる M や S を配置しています。 ParDo の of メソッドを呼びだして得られた変数に対して、 withOutputTags を呼びだすことでこのパイプラインが分岐することを宣言しています。 ここでは二つに分岐していますが、三つや四つ、それよりも多くのPCollectionに分岐できます。 分岐されたパイプラインから MAIN でタグ付けされた PCollection を取り出しています。 1. では文字列の先頭が M になっているものがこれにあたります。 分岐されたパイプラインから SUB でタグ付けされた PCollection を取り出しています。 1. では文字列の先頭が S になっているものがこれにあたります。 まとめ Apache Beamを使った バッチ処理 を書く上で最も汎用性の高い ParDo を使ったスタイルをいくつか紹介しました。 今日紹介したスタイルは、それぞれ専用の API が用意されていますが、必要に応じて API を覚えなおすのはやや面倒です。 例えば、型を1:1で変換するなら、 MapElements という専用の API があります。フィルターしたいなら Filter があります。 ParDo には、この記事では紹介しきれなかった便利な機能が他にもありますので是非試してみてください。 Google Dataflowは非常に巨大なデータを バッチ処理 するための実 行基 盤として非常に安価に利用できる上にハイパフォーマンスに動作する環境です。 例えば、筆者の業務ではGCSにおいたログファイルをBigQueryへ投入する手段としてDataflowを利用しています。テラバイトクラスのログファイルが分散処理によって数十分でDBに投入されていく様子は圧巻というほかありません。 この記事を読んだ皆様がDataflowを使って、筆者が受けた感銘を共有していただけたら非常に嬉しいです。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募をお待ちしています。 社内SE(DX推進エンジニア) 執筆: @sato.taichi 、レビュー: @handa.kenta ( Shodo で執筆されました )
みなさんこんにちは、 電通国際情報サービス (ISID)コーポレート本部 システム推進部の佐藤太一です。 このエントリでは Google Dataflowを使ったデータ分析パイプライン構築において中心的な API の使い方について説明します。 Google Dataflowとはなにか Dataflowの開発環境構築 GradleによるDataflowプロジェクトの作り方 Apache Beamの基礎 Pipelineについて PCollectionについて ParDoを使った逐次処理の書き方 Dataflowによるユニットテストの書き方 フィルター フィルターのテスト 値の増幅処理 増幅処理のテスト PCollectionの分岐 PCollectionの分岐をテストする まとめ Google Dataflowとはなにか Google DataflowはいわゆるExtract/Transform/Load(ETL)ツールの一種です。 Apache Beam という バッチ処理 基盤を GCP の分散処理環境で動かしてくれます。 Apache Beam自体は、 Apache Flink や Apache Spark 、 Hazelcast Jet といったオンプレミスで動作する実行環境を利用することもできます。 Apache Beamでは、 Java や Python 、Goといった言語で処理を記述できますが、今回は Java を使って説明します。 Dataflowの開発環境構築 まずは、Dataflowの開発環境を作っていきましょう。 開発環境として使うマシンには、事前にJava17とGradle7.5以上をインストールしておいてください。 GradleによるDataflowプロジェクトの作り方 最初にプロジェクト全体を格納するための ディレクト リを作成しましょう。 ここからは、この記事内でシェルコマンドを実行するよう説明している部分では、必ずこのルート ディレクト リで実行してください。 作るプロジェクトは、説明のために dataflow-example とします。作った dataflow-example ディレクト リの中で、以下のコマンドを実行して最小限のプロジェクトを作成します。 gradle init --type basic --dsl kotlin --project-name dataflow-example --incubating 最小限とはいえ、Gradle Wrapperとなる シェルスクリプト やgit用の設定ファイルが生成されていますね。 この中から、 build.gradle.kts を以下のように編集します。 plugins { id("java") } group = "com.example.dataflow" version = "0.1.0-SNAPSHOT" java.toolchain { languageVersion.set(JavaLanguageVersion.of(17)) // 1. } repositories { mavenCentral() maven("https://packages.confluent.io/maven/") // 2. } dependencies { var beamVersion = "2.41.0" // 3. var slf4jVersion = "1.7.36" implementation(platform("com.google.cloud:libraries-bom:25.4.0")) // 4. implementation("org.apache.beam:beam-sdks-java-core:${beamVersion}") // 5. implementation("org.apache.beam:beam-sdks-java-io-google-cloud-platform:${beamVersion}") // 5. implementation("org.apache.beam:beam-runners-google-cloud-dataflow-java:${beamVersion}") // 5. implementation("org.apache.commons:commons-csv:1.9.0") implementation("org.slf4j:slf4j-api:${slf4jVersion}") implementation("org.slf4j:slf4j-jdk14:${slf4jVersion}") testImplementation("junit:junit:4.13.2") // 6. testImplementation("org.apache.beam:beam-runners-direct-java:${beamVersion}") // 7. } tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" } このビルド スクリプト で利用する コンパイラ やランタイムのバージョン番号を指定しています。 Gradleを実行している Java ランタイムのバージョンが開発者ごとにズレていても、 コンパイル やテストに使う Java ランタイムは統一できるということです。ビルドの再現性が高まりますので必ず設定しましょう。 この機能を使うと、必要に応じてGradleがビルド済みの JDK を自動的にダウンロードしてくれます。 デフォルトでは Adoptium を使います。 依存ライブラリをダウンロードする先を宣言しています。 最初に指定しているのは Maven のデフォルト アーティファクト リポジトリ である Maven Central Repository です。 二つ目に指定しているのはconfluentが公開している リポジトリ です。これはKafka関連のライブラリで、この リポジトリ 内にしかないものがあるからです。 Apache Beamのバージョンを参照する依存性がいくつかあるので、ここでは変数として切り出しています。 Dataflowを動かすために必要な GCP の SDK に対する依存性を宣言しています。 GradleのPlatform機能 を使っていますね。 Apache Beamに対する依存性を宣言しています。後半の二つは GCP でBeamを動かすために必要な依存性です。 ユニットテスト 用の依存性としてJUnit4を指定しています。 JUnit の最新版はJUnit5系ですが、記事執筆時点において Apache BeamはJUnit5をサポートしていません。cf. JUnit5 support Apache Beam用の ユニットテスト ライブラリに対する依存性を宣言しています。 これでDataflow用のローカルビルド環境の構築は完了です。 Apache Beamの基礎 Apache Beamを理解するなら、まずはPipelineとPCollectionをしっかり理解してください。 他のコンセプトについては、Dataflowのドキュメントを参照してください。 * Apache Beam のプログラミング モデル Pipelineについて Pipelineは複数のステップから構成される処理全体を表すオブジェクトで、データの読み取り処理から始まりフィルターや変換を経て、出力処理までを行います。一つのパイプラインが一つのジョブとなります。 Pipelineを構成する各ステップは、実行環境が必要に応じて分散処理してくれます。つまり、各ステップを効率よく動作させるには、それぞれのステップが全く違ったプロセスの上で非同期に実行されても問題がおきないようにしましょう。 具体的には、処理単位になるデータの独立性をできるかぎり高めるようにします。つまり、 RDB における非正規化を積極的に行うようなデータの持ち方をします。 順序に強い整合性を求める書き方もできますが、そうすると分散処理環境がもつ性能を十分に引き出せません。 PCollectionについて PCollectionはPipelineを流れるデータの集合を表すオブジェクトです。 逐次的に要素を扱えるので Java のCollection Frameworkと似ていますが、PCollectionの API だけではデータの開始と終了を明示的に調べられません。 また、PCollectionに格納されている要素は、分散処理環境内における実行環境の都合で シリアライズ されたりコピーされる可能性があります。つまり、処理に必要な情報は全て要素内に内包する必要があります。 ParDoを使った逐次処理の書き方 基本的な概念が分かった所で本題に入っていきましょう。 最初に作るのは、PCollectionを流れる要素を1:1で変換していく処理です。 この図は四角い枠がPCollectionで、〇が各要素、矢印が処理です。つまり、全体がPipelineとなります。 今回の記事では、全てのコードをテストコードとして実装しますので、以下のように ディレクト リを作成します。 mkdir src/test/java/com/example/dataflow 出来た ディレクト リに CsvFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; import java.util.*; public class CsvFn extends DoFn<String, List<String>> { // 1. @ProcessElement // 2. public void processElement(ProcessContext c) throws Exception { // 3. var element = c.element(); // 4. var list = Arrays.asList(element.split(",")); // 5. c.output(list); // 6. } } この処理では、単一の文字列を入力すると、それをカンマ区切りで分割したリストとして後続の処理に引き渡します。 逐次処理を実装する際に使うクラスは DoFn を継承します。 一つ目の型パラメータは、各入力要素を表す型を設定します。ここでは String を設定しています。 二つ目の型パラメータは、各出力要素を表す型を設定します。ここでは List<String> を設定しています。 逐次処理を行うメソッドは @ProcessElement アノテーション を付与します。 逐次処理を行うメソッドのアクセス修飾子は public 、戻り値は void です。送出される例外としては Exception を定義しておきます。 なお、このメソッドの中から例外を送出するとジョブ全体が停止します。 ProcessContext の element メソッドを呼ぶと、 DoFn を継承する際に設定した一番目の型パラメータの変数が得られます。ここでは String 型の変数が得られるわけです。 String の split メソッドを呼びだして得られた配列を List に格納しています。 ProcessContext の output メソッドを呼ぶ際には、 DoFn を継承する際に設定した二番目の型パラメータの変数を渡します。ここでは既に作成済みの list を渡していますね。 Dataflowによる ユニットテスト の書き方 次は、逐次処理を ユニットテスト してみましょう。 CsvFn.java と同じ ディレクト リ内に CsvFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class CsvFnTest { static final List<String> values = List.of( "foo,bar,baz", "fo1,ba2,ba3", "ba1,ba2,ba3"); @Rule public TestPipeline pipeline = TestPipeline.create(); // 1. @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) // 2. .apply(ParDo.of(new CsvFn())); // 3. PAssert.that(output).containsInAnyOrder( // 4. List.of("foo", "bar", "baz"), List.of("ba1", "ba2", "ba3"), // 5. List.of("fo1", "ba2", "ba3") ); pipeline.run().waitUntilFinish(); // 6. } } ユニットテスト 用のパイプラインを生成しています。パイプラインを構成するための共通処理があるので @Rule を付与しています。 Create の of メソッドを使って文字列のリストをパイプラインに流せる形に変換しています。ここでは、 List の各要素がパイプラインを流れていきます。 ParDo の of メソッドに先ほど実装した CsvFn を インスタンス 化して渡しています。これによって、パイプラインを流れる各要素ごとに CsvFn の processElement メソッドが呼びだされます。 パイプラインを流れる要素が正しく変換されているか確認するには、 Apache Beamで用意されている専用の PAssert を使います。ここでは containsInAnyOrder メソッドを使ってそれぞれの要素が正しくカンマ区切りで分解されたか確認しています。 values 変数として定義した要素の順序とは違った順序で要素を検証しています。これは、パイプラインを流れる要素の処理順序は保証されておらず、実行環境の都合で任意に入れ替わる可能性があることを意図しています。つまり、 Create の of メソッドで作った要素がそのままの順序で CsvFn の processElement メソッドに入ってくるとは限りません。 TestPipeline の run メソッドを呼びだした上で、さらに waitUntilFinish メソッドを呼んでパイプラインの処理が終わるのを待っています。デバッガで実行する際に注意してほしいのは、この時点で初めてパイプラインの処理が動き始めることです。つまり、 4. の時点では、まだ CsvFn の processElement メソッドは呼びだされません。 フィルター CsvFnでは単純な1:1の変換処理を実装しましたので、次はフィルター処理を実装してみましょう。 フィルター処理として作るのは、指定した長さよりも長い文字列だけを後続の処理に流すフィルターです。 CsvFn.java と同じ ディレクト リ内に FilterFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; public class FilterFn extends DoFn<String, String> { // 1. final int size; // 2. public FilterFn(int size) { this.size = size; } @ProcessElement public void processElement(ProcessContext c) { var element = c.element(); if (size < element.length()) { // 3. c.output(element); } } } ここで実装するのはフィルター処理なので、入力と出力の型は同じです。 コンスト ラク タで受け取った長さをメンバ変数として格納しています。 Apache BeamではPCollectionの要素だけでなく、各処理のステップを表すオブジェクトも実行環境の都合で直列化される可能性があります。つまり、メンバ変数としてはSerializableな型(もしくは、Externalizableな型)だけを定義できます。 条件分岐に基づいて ProcessContext の output メソッドを呼びだすかどうかを決めています。 フィルターのテスト では、フィルター処理を ユニットテスト してみましょう。 CsvFn.java と同じ ディレクト リ内に FilterFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class FilterFnTest { static final List<String> values = List.of( "alpha", "beta", "gamma"); @Rule public TestPipeline pipeline = TestPipeline.create(); @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) .apply(ParDo.of(new FilterFn(4))); PAssert.that(output).containsInAnyOrder("gamma", "alpha"); pipeline.run().waitUntilFinish(); } } 長さが4文字より大きい単語をフィルターできていますね。 値の増幅処理 次は、一つの入力から複数回の出力を行う処理を実装してみましょう。 増幅処理として作るのは、文字列をカンマ区切りで分割した各要素をそのまま後続に渡す処理です。 CsvFn.java と同じ ディレクト リ内に FlatValuesFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; import java.util.Arrays; public class FlatValuesFn extends DoFn<String, String> { // 1. @ProcessElement public void processElement(ProcessContext c) { var element = c.element(); var list = Arrays.asList(element.split(",")); list.forEach(c::output); // 2. } } ここで実装するのは増幅処理なので、入力と出力の型は同じです。 文字列を分割して得られた要素全てについて ProcessContext の ouput メソッドを呼びだしています。 増幅処理のテスト では、増幅処理を ユニットテスト してみましょう。 CsvFn.java と同じ ディレクト リ内に FlatValuesFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class FlatValuesFnTest { static final List<String> values = List.of( "foo,bar,baz", "fo1,ba2,ba3", "ba1,ba2,ba3"); @Rule public TestPipeline pipeline = TestPipeline.create(); @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) .apply(ParDo.of(new FlatValuesFn())); PAssert.that(output).containsInAnyOrder( "foo", "bar", "baz", "ba1", "ba2", "ba3", "fo1", "ba2", "ba3" ); pipeline.run().waitUntilFinish(); } } カンマ区切りで3つずつに分割できる要素を3回 FlatValuesFn で処理したので9つの要素が出力されていますね。 PCollectionの分岐 ここまでの処理では、処理の流れであるPCollection自体は1つのまま要素が流れていきました。 しかし、データの1カラム目だけを見て後続の処理を切り替えるといった処理構造を実現したくなることはあります。 ここでは、入力された文字列の1文字目を使って後続の処理を切り替えるためにPCollectionを分岐してみましょう。 CsvFn.java と同じ ディレクト リ内に BranchFn.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.values.TupleTag; import java.util.*; public class BranchFn extends DoFn<String, List<String>> { static final TupleTag<List<String>> MAIN = new TupleTag<>() { // 1. }; static final TupleTag<List<String>> SUB = new TupleTag<>() { }; @ProcessElement public void processElement(ProcessContext c) { var element = c.element(); var list = Arrays.asList(element.split(",")); if (list.get(0).equals("M")) { c.output(MAIN, list.subList(1, list.size()));       // 2. } else { c.output(SUB, list.subList(1, list.size())); } } } TupleTag は実行環境全体で一意のIDを付与する必要があります。ここでは、ややトリッキーなテクニックを使ってそれを実現しています。コンスト ラク タ呼び出しの後ろについている中括弧 {} によってインナークラスを作成していることがポイントです。実装の詳細が気になる方は是非コードを読んでみてください。 ProcessContext の ouput メソッドを呼びだす際に、 TupleTag を渡しています。これによって各要素にタグ付けをすることで、PCollectionの分岐を実現しているのです。 PCollectionの分岐をテストする では、分岐したPCollectionをどのように扱うのかテストコードで確認してみましょう。 CsvFn.java と同じ ディレクト リ内に BranchFnTest.java というファイルを以下の内容で作成します。 package com.example.dataflow; import org.apache.beam.sdk.testing.*; import org.apache.beam.sdk.transforms.*; import org.apache.beam.sdk.values.TupleTagList; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; @RunWith(JUnit4.class) public class BranchFnTest { static final List<String> values = List.of( "M,foo,bar,baz", "S,fo1,ba2,ba3", "M,ba1,ba2,ba3"); // 1. @Rule public TestPipeline pipeline = TestPipeline.create(); @Test public void testSimplePipeline() throws Exception { var output = pipeline .apply(Create.of(values)) .apply(ParDo.of(new BranchFn()) .withOutputTags(BranchFn.MAIN, TupleTagList.of(List.of(BranchFn.SUB))) // 2. ); PAssert.that(output.get(BranchFn.MAIN)).containsInAnyOrder( // 3. List.of("foo", "bar", "baz"), List.of("ba1", "ba2", "ba3") ); PAssert.that(output.get(BranchFn.SUB)).containsInAnyOrder( // 4. List.of("fo1", "ba2", "ba3") ); pipeline.run().waitUntilFinish(); } } テストデータとして、各要素の先頭に分岐の条件となる M や S を配置しています。 ParDo の of メソッドを呼びだして得られた変数に対して、 withOutputTags を呼びだすことでこのパイプラインが分岐することを宣言しています。 ここでは二つに分岐していますが、三つや四つ、それよりも多くのPCollectionに分岐できます。 分岐されたパイプラインから MAIN でタグ付けされた PCollection を取り出しています。 1. では文字列の先頭が M になっているものがこれにあたります。 分岐されたパイプラインから SUB でタグ付けされた PCollection を取り出しています。 1. では文字列の先頭が S になっているものがこれにあたります。 まとめ Apache Beamを使った バッチ処理 を書く上で最も汎用性の高い ParDo を使ったスタイルをいくつか紹介しました。 今日紹介したスタイルは、それぞれ専用の API が用意されていますが、必要に応じて API を覚えなおすのはやや面倒です。 例えば、型を1:1で変換するなら、 MapElements という専用の API があります。フィルターしたいなら Filter があります。 ParDo には、この記事では紹介しきれなかった便利な機能が他にもありますので是非試してみてください。 Google Dataflowは非常に巨大なデータを バッチ処理 するための実 行基 盤として非常に安価に利用できる上にハイパフォーマンスに動作する環境です。 例えば、筆者の業務ではGCSにおいたログファイルをBigQueryへ投入する手段としてDataflowを利用しています。テラバイトクラスのログファイルが分散処理によって数十分でDBに投入されていく様子は圧巻というほかありません。 この記事を読んだ皆様がDataflowを使って、筆者が受けた感銘を共有していただけたら非常に嬉しいです。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募をお待ちしています。 社内SE(DX推進エンジニア) 執筆: @sato.taichi 、レビュー: @handa.kenta ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、長い呪文は切り捨てられる編です。 からあげさんの Stable Diffusionの内容を理解するための情報・書籍の記事 の中に出てくる 最初の75個分以降の文章は、バッサリ捨てられてしまう記事 をみて、自分でも検証してみたのが今回の記事です。 これまで、意味のある区切りとして、カンマ(,)を使っていたのですが、カンマ(,)は75個の トーク ンの一つとして重複して数えられ、カンマ(,)を削っても出力結果はほとんど変わらないことを知って愕然としました。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 tokenizer 75個を超えるトークンは切り捨てられるのか 呪文のカンマを削っても出力結果に影響はないのか まとめ 仲間募集 Stable Diffusionの過去コンテンツ tokenizer 呪文を トーク ンに分解するには、tokenizerを作成します。必要なライブラリは次のようにインストールします。今回は、Notebookの例です。 !pip install transformers ftfy regex tokenizerの作成は次のように行います。 from transformers import CLIPTokenizer tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") 呪文を トーク ンに分解するには、次のように行います。 prompt = "anime of beautiful girl, detailed beautiful face, detailed hair, detailed perfect pupil of eyes, detailed cute mouth, detailed shoulders, detailed bust" tokens = tokenizer.tokenize(prompt) print(len(tokens), tokens[0:tokenizer.model_max_length-2]) トーク ン出力結果(改行版) 37 ['japanese</w>', 'anime</w>', 'of</w>', 'beautiful</w>', 'girl</w>', ',</w>', 'detailed</w>', 'beautiful</w>', 'face</w>', ',</w>', 'detailed</w>', 'hair</w>', ',</w>', 'detailed</w>', 'perfect</w>', 'pupil</w>', 'of</w>', 'eyes</w>', ',</w>', 'detailed</w>', 'cute</w>', 'mouth</w>', ',</w>', 'detailed</w>', 'shoulders</w>', ',</w>', 'detailed</w>', 'bust</w>'] 37の トーク ンに分解されたことがわかります。',</w>'に注目してください。これは、カンマ(,)です。カンマ(,)も トーク ンの一つにカウントされていることがわかります。 僕が実験した限り、カンマ(,)ありなしで、出力される結果に違いはありません。75個を超える トーク ンは切り捨てられることを考えると、カンマ(,)は呪文に含めないのが良いでしょう。 75個を超える トーク ンは切り捨てられるのか それでは、本当に「75個を超える トーク ンは切り捨てられるのか」実験してみましょう。 下記のようなちょうど75個の トーク ンの呪文を用意します。 prompt = "illustration of a beautiful girl, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ," tokens = tokenizer.tokenize(prompt) print(len(tokens), tokens[0:tokenizer.model_max_length-2]) image = pipe(prompt)["sample"][0] image トーク ン出力結果(改行版) 75 ['illustration</w>', 'of</w>', 'a</w>', 'beautiful</w>', 'girl</w>', ',</w>', 省略 ',</w>'] 確かに呪文は、75個の トーク ンで構成されていますね。この呪文の最後に以下の呪文を追加しましょう。75個を超える トーク ンが切り捨てられるなら、結果に影響はないはずです。 追加する呪文(改行版) deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k トーク ン出力結果(改行版) 109 ['illustration</w>', 'of</w>', 'a</w>', 'beautiful</w>', 'girl</w>', ',</w>', 省略 ',</w>'] 結果に影響はなかったですね。75個を超える トーク ンが切り捨てられることが確認できました。 呪文のカンマを削っても出力結果に影響はないのか 今度は、呪文のカンマを削っても出力結果に影響がないのかを検証してみましょう。 今回の呪文は、 美少女を高確率で出す呪文編 で使った呪文の改良版です。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed perfect pupil of eyes, detailed mouth, detailed shoulders, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed perfect pupil of eyes, detailed mouth, detailed shoulders, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k 改良したのは、detailed perfect pupil of eyesとdetailed shouldersの二箇所です。 イラストなら、前回の呪文でも大丈夫なのですが、写真で試すと目や腕が変になることがあったので改良しました。 目は、detailed beautiful eyesをdetailed perfect pupil of eyesに変えました。 腕は、detailed armsをdetailed shouldersに変えました。腕は変になりやすいので、指定しないほうが無難なようです。肩を指定することで腕が出力される回数が減りました。 トーク ン出力結果(改行版) 74 ['illustration</w>','of</w>','a</w>','beautiful</w>','girl</w>', ',</w>', 'detailed</w>','beautiful</w>','face</w>', ',</w>', 'detailed</w>','hair</w>', ',</w>', 'detailed</w>','perfect</w>','pupil</w>','of</w>','eyes</w>', ',</w>', 'detailed</w>','mouth</w>', ',</w>', 'detailed</w>','shoulders</w>', ',</w>', 'detailed</w>','bust</w>', ',</w>', 'looking</w>','far</w>','away</w>', ',</w>', 'shot</w>','diag', 'on', 'ally</w>', ',</w>', 'art','station</w>', ',</w>', 'deviantart</w>', ',</w>', 'concept</w>','art</w>', ',</w>', 'digital</w>','painting</w>', ',</w>', 'award</w>','-</w>','winning</w>', ',</w>', 'cinematic</w>','post','processing</w>', ',</w>', 'cinematic</w>','scene</w>', ',</w>', 'cinematic</w>','composition</w>', ',</w>', 'cinematic</w>','lighting</w>', ',</w>', 'ove', 'rex', 'pose</w>', ',</w>', 'ray</w>','tracing</w>', ',</w>', '8</w>','k</w>'] 画像出力結果 既に トーク ンの数は74(Max 75)。これ以上、詳細な指定ができなくなるのは問題です。出力結果を見ながら呪文を改良していきましょう。 ',</w>'は削れますね。 a beautiful girlのaも削れます。girlを見れば単数であることがわかるからです。僕の試した限りはtheも削ることができます。 diagonallyが、'diag', 'on', 'ally</w>'と3つに分かれて認識されています。これは、Stable Diffusionがdiagonallyという単語を認識できていないことを示しています。 この結果を見てdiagonallyが無効だと判断するのは早計です。Stable Diffusionは、ワードでない(</w>がついていない)連続した トーク ンを有効だと認識していることもあるからです。例えば、pixivは、'pi', 'xiv</w>'と別れていますが、pixivあり/なしで、出力結果は変わってきます。つまり、pixivの呪文は有効だということです。 ワードでない(</w>がついていない)連続した トーク ンは、あり/なしで何度も試しましょう。効果があると確信できるときだけ、呪文として採用しましょう。shot diagonally(斜めに撮影する)は、何度も試した結果効果が確信できなかったので、不採用にしました。 同様にワードでない(</w>がついていない)連続した トーク ンの'ove', 'rex', 'pose</w>'も不採用にしました。 award-winningは、'award</w>', '-</w>', 'winning</w>'に分解されていたので、真ん中の '-</w>'は不要と判断し、award winningにすることにしました。 改良版の呪文(横長、コピー&ペースト用)はこちらになります。 illustration of beautiful girl detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away artstation deviantart concept art digital painting award winning cinematic postprocessing cinematic scene cinematic composition cinematic lighting ray tracing 8k 改良版の閲覧用呪文(改行版) illustration of beautiful girl detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away artstation deviantart concept art digital painting award winning cinematic postprocessing cinematic scene cinematic composition cinematic lighting ray tracing 8k トーク ン出力結果(改行版) 45 ['illustration</w>', 'of</w>', 'beautiful</w>', 'girl</w>', 'detailed</w>', 'beautiful</w>', 'face</w>', 'detailed</w>', 'hair</w>', 'detailed</w>', 'perfect</w>', 'pupil</w>', 'of</w>', 'eyes</w>', 'detailed</w>', 'mouth</w>', 'detailed</w>', 'shoulders</w>', 'detailed</w>', 'bust</w>', 'looking</w>', 'far</w>', 'away</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'cinematic</w>', 'post', 'processing</w>', 'cinematic</w>', 'scene</w>', 'cinematic</w>', 'composition</w>', 'cinematic</w>', 'lighting</w>', 'ray</w>', 'tracing</w>', '8</w>', 'k</w>'] 画像出力結果 トーク ンの個数が74から45に圧縮されました。画像のクオリティは変わっていないことが確認できると思います。 まとめ 今回は、75個を超える トーク ンは、切り捨てられることを確認しました。 さらに、呪文を圧縮するテクニックとして、カンマ(、)を削る、ワードでない(</w>がついていない)連続した トーク ンが有効かどうかをチェックすることを学びました。 次回は、 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、長い呪文は切り捨てられる編です。 からあげさんの Stable Diffusionの内容を理解するための情報・書籍の記事 の中に出てくる 最初の75個分以降の文章は、バッサリ捨てられてしまう記事 をみて、自分でも検証してみたのが今回の記事です。 これまで、意味のある区切りとして、カンマ(,)を使っていたのですが、カンマ(,)は75個の トーク ンの一つとして重複して数えられ、カンマ(,)を削っても出力結果はほとんど変わらないことを知って愕然としました。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 tokenizer 75個を超えるトークンは切り捨てられるのか 呪文のカンマを削っても出力結果に影響はないのか まとめ 仲間募集 Stable Diffusionの過去コンテンツ tokenizer 呪文を トーク ンに分解するには、tokenizerを作成します。必要なライブラリは次のようにインストールします。今回は、Notebookの例です。 !pip install transformers ftfy regex tokenizerの作成は次のように行います。 from transformers import CLIPTokenizer tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") 呪文を トーク ンに分解するには、次のように行います。 prompt = "anime of beautiful girl, detailed beautiful face, detailed hair, detailed perfect pupil of eyes, detailed cute mouth, detailed shoulders, detailed bust" tokens = tokenizer.tokenize(prompt) print(len(tokens), tokens[0:tokenizer.model_max_length-2]) トーク ン出力結果(改行版) 37 ['japanese</w>', 'anime</w>', 'of</w>', 'beautiful</w>', 'girl</w>', ',</w>', 'detailed</w>', 'beautiful</w>', 'face</w>', ',</w>', 'detailed</w>', 'hair</w>', ',</w>', 'detailed</w>', 'perfect</w>', 'pupil</w>', 'of</w>', 'eyes</w>', ',</w>', 'detailed</w>', 'cute</w>', 'mouth</w>', ',</w>', 'detailed</w>', 'shoulders</w>', ',</w>', 'detailed</w>', 'bust</w>'] 37の トーク ンに分解されたことがわかります。',</w>'に注目してください。これは、カンマ(,)です。カンマ(,)も トーク ンの一つにカウントされていることがわかります。 僕が実験した限り、カンマ(,)ありなしで、出力される結果に違いはありません。75個を超える トーク ンは切り捨てられることを考えると、カンマ(,)は呪文に含めないのが良いでしょう。 75個を超える トーク ンは切り捨てられるのか それでは、本当に「75個を超える トーク ンは切り捨てられるのか」実験してみましょう。 下記のようなちょうど75個の トーク ンの呪文を用意します。 prompt = "illustration of a beautiful girl, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ," tokens = tokenizer.tokenize(prompt) print(len(tokens), tokens[0:tokenizer.model_max_length-2]) image = pipe(prompt)["sample"][0] image トーク ン出力結果(改行版) 75 ['illustration</w>', 'of</w>', 'a</w>', 'beautiful</w>', 'girl</w>', ',</w>', 省略 ',</w>'] 確かに呪文は、75個の トーク ンで構成されていますね。この呪文の最後に以下の呪文を追加しましょう。75個を超える トーク ンが切り捨てられるなら、結果に影響はないはずです。 追加する呪文(改行版) deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k トーク ン出力結果(改行版) 109 ['illustration</w>', 'of</w>', 'a</w>', 'beautiful</w>', 'girl</w>', ',</w>', 省略 ',</w>'] 結果に影響はなかったですね。75個を超える トーク ンが切り捨てられることが確認できました。 呪文のカンマを削っても出力結果に影響はないのか 今度は、呪文のカンマを削っても出力結果に影響がないのかを検証してみましょう。 今回の呪文は、 美少女を高確率で出す呪文編 で使った呪文の改良版です。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed perfect pupil of eyes, detailed mouth, detailed shoulders, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed perfect pupil of eyes, detailed mouth, detailed shoulders, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k 改良したのは、detailed perfect pupil of eyesとdetailed shouldersの二箇所です。 イラストなら、前回の呪文でも大丈夫なのですが、写真で試すと目や腕が変になることがあったので改良しました。 目は、detailed beautiful eyesをdetailed perfect pupil of eyesに変えました。 腕は、detailed armsをdetailed shouldersに変えました。腕は変になりやすいので、指定しないほうが無難なようです。肩を指定することで腕が出力される回数が減りました。 トーク ン出力結果(改行版) 74 ['illustration</w>','of</w>','a</w>','beautiful</w>','girl</w>', ',</w>', 'detailed</w>','beautiful</w>','face</w>', ',</w>', 'detailed</w>','hair</w>', ',</w>', 'detailed</w>','perfect</w>','pupil</w>','of</w>','eyes</w>', ',</w>', 'detailed</w>','mouth</w>', ',</w>', 'detailed</w>','shoulders</w>', ',</w>', 'detailed</w>','bust</w>', ',</w>', 'looking</w>','far</w>','away</w>', ',</w>', 'shot</w>','diag', 'on', 'ally</w>', ',</w>', 'art','station</w>', ',</w>', 'deviantart</w>', ',</w>', 'concept</w>','art</w>', ',</w>', 'digital</w>','painting</w>', ',</w>', 'award</w>','-</w>','winning</w>', ',</w>', 'cinematic</w>','post','processing</w>', ',</w>', 'cinematic</w>','scene</w>', ',</w>', 'cinematic</w>','composition</w>', ',</w>', 'cinematic</w>','lighting</w>', ',</w>', 'ove', 'rex', 'pose</w>', ',</w>', 'ray</w>','tracing</w>', ',</w>', '8</w>','k</w>'] 画像出力結果 既に トーク ンの数は74(Max 75)。これ以上、詳細な指定ができなくなるのは問題です。出力結果を見ながら呪文を改良していきましょう。 ',</w>'は削れますね。 a beautiful girlのaも削れます。girlを見れば単数であることがわかるからです。僕の試した限りはtheも削ることができます。 diagonallyが、'diag', 'on', 'ally</w>'と3つに分かれて認識されています。これは、Stable Diffusionがdiagonallyという単語を認識できていないことを示しています。 この結果を見てdiagonallyが無効だと判断するのは早計です。Stable Diffusionは、ワードでない(</w>がついていない)連続した トーク ンを有効だと認識していることもあるからです。例えば、pixivは、'pi', 'xiv</w>'と別れていますが、pixivあり/なしで、出力結果は変わってきます。つまり、pixivの呪文は有効だということです。 ワードでない(</w>がついていない)連続した トーク ンは、あり/なしで何度も試しましょう。効果があると確信できるときだけ、呪文として採用しましょう。shot diagonally(斜めに撮影する)は、何度も試した結果効果が確信できなかったので、不採用にしました。 同様にワードでない(</w>がついていない)連続した トーク ンの'ove', 'rex', 'pose</w>'も不採用にしました。 award-winningは、'award</w>', '-</w>', 'winning</w>'に分解されていたので、真ん中の '-</w>'は不要と判断し、award winningにすることにしました。 改良版の呪文(横長、コピー&ペースト用)はこちらになります。 illustration of beautiful girl detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away artstation deviantart concept art digital painting award winning cinematic postprocessing cinematic scene cinematic composition cinematic lighting ray tracing 8k 改良版の閲覧用呪文(改行版) illustration of beautiful girl detailed beautiful face detailed hair detailed perfect pupil of eyes detailed mouth detailed shoulders detailed bust looking far away artstation deviantart concept art digital painting award winning cinematic postprocessing cinematic scene cinematic composition cinematic lighting ray tracing 8k トーク ン出力結果(改行版) 45 ['illustration</w>', 'of</w>', 'beautiful</w>', 'girl</w>', 'detailed</w>', 'beautiful</w>', 'face</w>', 'detailed</w>', 'hair</w>', 'detailed</w>', 'perfect</w>', 'pupil</w>', 'of</w>', 'eyes</w>', 'detailed</w>', 'mouth</w>', 'detailed</w>', 'shoulders</w>', 'detailed</w>', 'bust</w>', 'looking</w>', 'far</w>', 'away</w>', 'art', 'station</w>', 'deviantart</w>', 'concept</w>', 'art</w>', 'digital</w>', 'painting</w>', 'award</w>', 'winning</w>', 'cinematic</w>', 'post', 'processing</w>', 'cinematic</w>', 'scene</w>', 'cinematic</w>', 'composition</w>', 'cinematic</w>', 'lighting</w>', 'ray</w>', 'tracing</w>', '8</w>', 'k</w>'] 画像出力結果 トーク ンの個数が74から45に圧縮されました。画像のクオリティは変わっていないことが確認できると思います。 まとめ 今回は、75個を超える トーク ンは、切り捨てられることを確認しました。 さらに、呪文を圧縮するテクニックとして、カンマ(、)を削る、ワードでない(</w>がついていない)連続した トーク ンが有効かどうかをチェックすることを学びました。 次回は、 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター の山下です。 今回は、TypeScriptを使って、gRPCのアプリケーションを開発する際の方法について紹介します。 gRPCとProtocol Buffers gRPCは Google の開発した様々な環境で動作する オープンソース のRPC フレームワーク です。 gRPCは負荷分散、トレース、ヘルスチェック、認証などの機能をサポートし、効率的な通信の仕組みを提供しています。 gRPCは、Protocol Buffersというインターフェース記述言語(IDL)を用いてサービスを定義して利用します。このProtocol BuffersもgRPCと同じく Google が開発しています。gRPCでアプリケーションを開発する際は、まずProtocol Buffersから通信に関するプログラムを自動的に構築して開発することになります。 Protocol Buffersは多くの言語をサポートしています。今回利用するTypeScriptを直接サポートはしていませんが、Node.jsをサポートしているので問題なく利用できます。 詳細な情報はgRPCの公式サイトを参照してください。 https://grpc.io/ Protocol Buffersについての詳細な情報は以下のURLを参照してください。 https://developers.google.com/protocol-buffers この記事では、Node.jsのサポートを利用してTypeScriptのアプリケーション開発の手順について解説します。 gRPCの通信方式 gRPCでは4種類の通信方式がある。 Unary RPC 1つのリク エス トに対して一つのレスポンスを返す一般的な通信です。 Server streaming RPC クライアントから送られてきた一つのリク エス トに対して、サーバは複数回に分けてレスポンスを返す通信方式です。 Client streaming RPC クライアントからリク エス トを分割して送る方式でサーバーはすべてのリク エス トを受け取ってからレスポンスを返す方式です。 Bidirectional streaming RPC サーバーとクライアントが一つのコネクションを確立しお互いに任意のタイミングでリク エス トとレスポンスを送りあう通信方式です。 また、gRPCでは、ブラウザで利用するgRPC WebとHTTP/2を利用して通信を行うgRPC over HTTP/2の2種類の通信方式が存在しています。 gRPC over WEBはブラウザを中心に策定された仕様となっています。このため現在は、Client streaming RPC、Bidirectional streaming RPCを行うことが出来ません。 この記事では、このうちHTTP/2を▼使ったUnary RPCの開発手順について紹介します。 TypeScriptを用いたgRPCの開発手順 TypeScriptを用いたgRPCアプリケーションは以下のような流れで開発します。 Protocol Buffersの定義を proto ファイルで行う protoc プログラムを用いて proto ファイルから ソースコード を生成する 生成したプログラムにロジックを追加して完成させる という流れになります。 ここでは、その流れを順番に追ってみます。 Protocol Buffers でサービスとメッセージを記述する Protocol Buffersではサービスとメソッド(rpc名と呼ぶ方が適切かもしれませんが、本記事ではメソッドで統一します)を定義します。メソッドが API の概念に近いものとなり、メソッドが通信でやりとりする内容をメッセージとして定義することとなります。 Protocol Buffersの例を以下に示します。なおProtocol Buffersにはバージョンがあり本記事ではバージョン3を想定しています。 syntax = "proto3"; message HelloRequest { string name = 1; } message HelloResponse { string result =1; } service Hello { rpc hello(HelloRequest) returns (HelloResponse); } 上記の定義では、 Hello というサービスと hello メソッド、 HelloRequest と HelloResponse というメッセージが定義されています。 。 hello を呼びだす場合には、 HelloRequest を引数として呼び出し、その返り値としては HelloResponse というメッセージが返ってくることを表しています。 なお、ここではProtocol Buffersの文法の詳細を述べません。詳細については、公式のドキュメントを参照してください。 https://developers.google.com/protocol-buffers/docs/proto3 記載する際は以下のスタイルガイドが参考になります。 https://developers.google.com/protocol-buffers/docs/style スタイルガイドには以下のようにサービス名、メッセージ名とフィールド名についての 命名 の規則が記載されています。 Use CamelCase (with an initial capital) for message names – for example, SongServerRequest. Use underscore_separated_names for field names (including oneof field and extension names) – for example, song_name. このガイドの内容に従わなくても問題はおきません。しかし、従っていない場合は生成される ソースコード が不自然なものとなってしまいます。 例えば、フィールド名を userId のような名前を付けた場合、これに対応するメソッドやプロパティの名前は getUserid 、 setUserid といった形になってしまいます。 一方で user_id というフィールド名を用いた場合は getUserId 、 setUserId という名前で生成されます。コードの読みやすさなどの観点からフィールド名にはスネークケースを用いる方が良さそうですね。 Protocol Buffersの更新について Protocol Buffersのprotoファイルを更新して、メッセージのフォーマットを変更する場合には注意が必要となります。 特に、フィールド番号は安易に変更、削除してしまうと古いプログラムと新しいプログラムでメッセージのフォーマットが一致しなくなり通信できなくなってしまうので注意が必要です。 極力新しいフィールドを追加していく形で更新していく事が望ましいです。 また削除も可能であれば避け OBSOLETE_ といった プレフィックス をつけて残しておく事が望ましいです。 後々誤って削除したフィールド番号が再利用されてしまうといったトラブルを回避するためです。 また各種メッセージの種類には互換性があり、同じフィールド番号でもメッセージの種類の変更は可能な場合があります。 その他の注意点については公式のドキュメントを熟読し、慎重に更新を行っていく必要があるので注意してください。 https://developers.google.com/protocol-buffers/docs/proto3#updating Protocol Buffersから ソースコード を生成する protocプログラムとそのオプションについて Protocol Buffersからコードを生成するプログラム( protoc )は、オプションの記述方法に注意が必要です。 protoc を利用する際は、 xxx_out=.... というオプションが並ぶことになります。 このオプションは、以下のように解釈します。 --xxx_out は プラグイン と出力先の指定を意味します。これは、 proto-gen-xxx という プラグイン 名の場合は、 xxx_out という対応関係になっています。 proto-gen-go プラグイン なら --go_out 。 proto-gen-grpc-gateway プラグイン なら --grpc-gateway_out という具合になっています。 プラグイン 自体もオプションも同時に指定することが出来て、 --xxx_out=プラグインのオプション:出力先 という形になります。 例えば、 --js_out=import_style=commonjs,binary:${PROTO_DEST} と書いてある場合を考えます。 これは、 js_out プラグイン に import_style=commonjs,binary(=true) という引数を渡すことになります。そして、その出力先は ${PROTO_DEST} ということです。 TypeScript用のprotocについて protoc は Node.js に対応していますが、TypeScriptの型定義などを直接生成する機能は持っていません。 今回は、 grpc_tools_node_protoc_ts を用いて protoc の生成した Node.js の ソースコード に型定義を自動生成して利用します。これは、 grpc-tools というgRPC公式に含まれている grpc_tools_node_protoc というツールを拡張したものです。 以下が公式ドキュメントとその使い方です。 https://github.com/agreatfool/grpc_tools_node_protoc_ts#how-to-use # ソースコードの出力先 PROTO_DEST=./src/proto # protoファイルが置いてあるディレクトリ PROTO_DIR=./proto # Protocol BuffersからnodeのgRPCコードを自動生成 grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=${PROTO_DEST} \ --plugin=protoc-gen-grpc=$(which grpc_tools_node_protoc_plugin) \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* # typescript用の型定義を作成 grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=${PROTO_DEST} \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* もしくは以下のように一括で生成する方法もあります。 # 定義と実装を同時に生成 yarn run grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=grpc_js:${PROTO_DEST} \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=grpc_js:${PROTO_DEST} \ -I ${PROTO_DIR} \ ${PROTO_DIR}/*.proto 生成されたプログラムを利用してみる Hello サービスの簡単なサーバ、クライアントの実装を以下に示します。 通信に関わる処理は全てgRPC側が行ってくれているため、実際に記述する部分はロジックに対応する部分だけとなります。通信に用いるメッセージを組み立てる関数も自動生成されているので、それを利用して構築できます。 // サーバプログラム import * as grpc from '@grpc/grpc-js' ; import { sendUnaryData } from '@grpc/grpc-js/build/src/server-call' ; import { HelloRequest , HelloResponse } from '../proto/hello_pb' ; import { HelloService } from '../proto/hello_grpc_pb' ; const HelloServer = { hello: ( call: grpc.ServerUnaryCall < HelloRequest , HelloResponse >, callback: sendUnaryData < HelloResponse >) : void => { const request = call.request ; const response = new HelloResponse (); console .log ( "Message from client" ); response.setResult ( "Hello," + request.getName ()) callback ( null , response ); } } function serve () : void { const server = new grpc.Server (); server.addService ( HelloService , HelloServer ); server.bindAsync ( `localhost:6543` , grpc.ServerCredentials.createInsecure (), ( err , port ) => { if ( err ) { throw err ; } console .log ( `Listening on ${ port } ` ); server.start (); } ); } serve (); // クライアントプログラム import * as grpc from '@grpc/grpc-js' ; import { HelloClient } from '../proto/hello_grpc_pb' ; import { HelloRequest , HelloResponse } from '../proto/hello_pb' ; function hello () : Promise < HelloResponse > { const client = new HelloClient ( `localhost:6543` , grpc.credentials.createInsecure (), ); // HelloRequestを作るためのクラス、メソッドが用意されているのでそれを用いてメッセージを作成する const request = new HelloRequest (); request.setName ( "ISID" ); // サーバに対してサービスの実行を要求する return new Promise < HelloResponse >(( resolve , reject ) => { console .log ( "Send Hello Message" ); client.hello ( request , ( err , response ) => { if ( err ) { return reject ( err ); } // ここで結果を受け取っている console .log ( "Receive Message" ); return resolve ( response ); } ); } ); } (async () => { console .log ( "Client Start" ); const result = await hello (); console .log ( result.getResult ()); } )(); 実際のプロジェクト構成と動作例 ここでは、今回のサンプルで利用したプロジェクトの構成を紹介します。 プロジェクト構成例 ディレクト リの構成は以下のような構成です。 . ├── package.json ├── proto │   └── hello.proto ├── scripts │   └── build-protos.sh ├── src │   ├── client │   │   └── index.ts │   ├── proto │   └── server │   └── index.ts └── tsconfig.json また、package. json の中身は以下のような内容です。 { " name ": " grpc-hello ", " version ": " 1.0.0 ", " main ": " index.js ", " license ": " MIT ", " dependencies ": { } , " scripts ": { " lint ": " yarn run eslint --fix --ext .ts src ", " clean ": " rm -rf ./dist && rm -rf ./src/proto && mkdir -p ./src/proto ", " build ": " sh ./scripts/build-protos.sh ./hello.proto ./src/proto && yarn run tsc; cp -r ./src/proto ./dist/src/proto " , } , " devDependencies ": { " @grpc/grpc-js ": " ^1.4.2 ", " @types/eslint ": " ^7.28.2 ", " @types/google-protobuf ": " ^3.15.5 ", " @types/node ": " ^16.11.7 ", " @typescript-eslint/eslint-plugin ": " ^5.3.0 ", " @typescript-eslint/parser ": " ^5.3.0 ", " eslint ": " ^8.1.0 ", " grpc-tools ": " ^1.11.2 ", " grpc_tools_node_protoc_ts ": " ^5.3.2 ", " ts-node ": " ^10.4.0 ", " tsconfig-paths ": " ^3.11.0 ", " typescript ": " ^4.4.4 " } } 動作確認 まず yarn build で Protocol Buffers からコード生成、TypeScriptから JavaScript へのトランスパイルを実行します。 $ yarn $ yarn build サーバの起動 $ node ./dist/src/server/ Listening on 6543 クライアントの実行 $ node ./dist/src/client Client Start Send Hello Message Receive Message Hello,ISID サーバー側のログも確認すると Message from client というようなログが出ています。 gRPCを使った通信プログラムが実装できてそうです。 まとめ 今回は、gRPCを用いたアプリケーション開発の方法やその注意点について紹介しました。 gRPCは Protocol Buffers を定義するだけで高品質な通信プログラムが実装できる素晴しい技術ですね。 機会があれば積極的に利用していきたいと思います。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。 ソリューションアーキテクト 執筆: @yamashita.tsuyoshi 、レビュー: @handa.kenta ( Shodo で執筆されました )
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター の山下です。 今回は、TypeScriptを使って、gRPCのアプリケーションを開発する際の方法について紹介します。 gRPCとProtocol Buffers gRPCは Google の開発した様々な環境で動作する オープンソース のRPC フレームワーク です。 gRPCは負荷分散、トレース、ヘルスチェック、認証などの機能をサポートし、効率的な通信の仕組みを提供しています。 gRPCは、Protocol Buffersというインターフェース記述言語(IDL)を用いてサービスを定義して利用します。このProtocol BuffersもgRPCと同じく Google が開発しています。gRPCでアプリケーションを開発する際は、まずProtocol Buffersから通信に関するプログラムを自動的に構築して開発することになります。 Protocol Buffersは多くの言語をサポートしています。今回利用するTypeScriptを直接サポートはしていませんが、Node.jsをサポートしているので問題なく利用できます。 詳細な情報はgRPCの公式サイトを参照してください。 https://grpc.io/ Protocol Buffersについての詳細な情報は以下のURLを参照してください。 https://developers.google.com/protocol-buffers この記事では、Node.jsのサポートを利用してTypeScriptのアプリケーション開発の手順について解説します。 gRPCの通信方式 gRPCでは4種類の通信方式がある。 Unary RPC 1つのリク エス トに対して一つのレスポンスを返す一般的な通信です。 Server streaming RPC クライアントから送られてきた一つのリク エス トに対して、サーバは複数回に分けてレスポンスを返す通信方式です。 Client streaming RPC クライアントからリク エス トを分割して送る方式でサーバーはすべてのリク エス トを受け取ってからレスポンスを返す方式です。 Bidirectional streaming RPC サーバーとクライアントが一つのコネクションを確立しお互いに任意のタイミングでリク エス トとレスポンスを送りあう通信方式です。 また、gRPCでは、ブラウザで利用するgRPC WebとHTTP/2を利用して通信を行うgRPC over HTTP/2の2種類の通信方式が存在しています。 gRPC over WEBはブラウザを中心に策定された仕様となっています。このため現在は、Client streaming RPC、Bidirectional streaming RPCを行うことが出来ません。 この記事では、このうちHTTP/2を▼使ったUnary RPCの開発手順について紹介します。 TypeScriptを用いたgRPCの開発手順 TypeScriptを用いたgRPCアプリケーションは以下のような流れで開発します。 Protocol Buffersの定義を proto ファイルで行う protoc プログラムを用いて proto ファイルから ソースコード を生成する 生成したプログラムにロジックを追加して完成させる という流れになります。 ここでは、その流れを順番に追ってみます。 Protocol Buffers でサービスとメッセージを記述する Protocol Buffersではサービスとメソッド(rpc名と呼ぶ方が適切かもしれませんが、本記事ではメソッドで統一します)を定義します。メソッドが API の概念に近いものとなり、メソッドが通信でやりとりする内容をメッセージとして定義することとなります。 Protocol Buffersの例を以下に示します。なおProtocol Buffersにはバージョンがあり本記事ではバージョン3を想定しています。 syntax = "proto3"; message HelloRequest { string name = 1; } message HelloResponse { string result =1; } service Hello { rpc hello(HelloRequest) returns (HelloResponse); } 上記の定義では、 Hello というサービスと hello メソッド、 HelloRequest と HelloResponse というメッセージが定義されています。 。 hello を呼びだす場合には、 HelloRequest を引数として呼び出し、その返り値としては HelloResponse というメッセージが返ってくることを表しています。 なお、ここではProtocol Buffersの文法の詳細を述べません。詳細については、公式のドキュメントを参照してください。 https://developers.google.com/protocol-buffers/docs/proto3 記載する際は以下のスタイルガイドが参考になります。 https://developers.google.com/protocol-buffers/docs/style スタイルガイドには以下のようにサービス名、メッセージ名とフィールド名についての 命名 の規則が記載されています。 Use CamelCase (with an initial capital) for message names – for example, SongServerRequest. Use underscore_separated_names for field names (including oneof field and extension names) – for example, song_name. このガイドの内容に従わなくても問題はおきません。しかし、従っていない場合は生成される ソースコード が不自然なものとなってしまいます。 例えば、フィールド名を userId のような名前を付けた場合、これに対応するメソッドやプロパティの名前は getUserid 、 setUserid といった形になってしまいます。 一方で user_id というフィールド名を用いた場合は getUserId 、 setUserId という名前で生成されます。コードの読みやすさなどの観点からフィールド名にはスネークケースを用いる方が良さそうですね。 Protocol Buffersの更新について Protocol Buffersのprotoファイルを更新して、メッセージのフォーマットを変更する場合には注意が必要となります。 特に、フィールド番号は安易に変更、削除してしまうと古いプログラムと新しいプログラムでメッセージのフォーマットが一致しなくなり通信できなくなってしまうので注意が必要です。 極力新しいフィールドを追加していく形で更新していく事が望ましいです。 また削除も可能であれば避け OBSOLETE_ といった プレフィックス をつけて残しておく事が望ましいです。 後々誤って削除したフィールド番号が再利用されてしまうといったトラブルを回避するためです。 また各種メッセージの種類には互換性があり、同じフィールド番号でもメッセージの種類の変更は可能な場合があります。 その他の注意点については公式のドキュメントを熟読し、慎重に更新を行っていく必要があるので注意してください。 https://developers.google.com/protocol-buffers/docs/proto3#updating Protocol Buffersから ソースコード を生成する protocプログラムとそのオプションについて Protocol Buffersからコードを生成するプログラム( protoc )は、オプションの記述方法に注意が必要です。 protoc を利用する際は、 xxx_out=.... というオプションが並ぶことになります。 このオプションは、以下のように解釈します。 --xxx_out は プラグイン と出力先の指定を意味します。これは、 proto-gen-xxx という プラグイン 名の場合は、 xxx_out という対応関係になっています。 proto-gen-go プラグイン なら --go_out 。 proto-gen-grpc-gateway プラグイン なら --grpc-gateway_out という具合になっています。 プラグイン 自体もオプションも同時に指定することが出来て、 --xxx_out=プラグインのオプション:出力先 という形になります。 例えば、 --js_out=import_style=commonjs,binary:${PROTO_DEST} と書いてある場合を考えます。 これは、 js_out プラグイン に import_style=commonjs,binary(=true) という引数を渡すことになります。そして、その出力先は ${PROTO_DEST} ということです。 TypeScript用のprotocについて protoc は Node.js に対応していますが、TypeScriptの型定義などを直接生成する機能は持っていません。 今回は、 grpc_tools_node_protoc_ts を用いて protoc の生成した Node.js の ソースコード に型定義を自動生成して利用します。これは、 grpc-tools というgRPC公式に含まれている grpc_tools_node_protoc というツールを拡張したものです。 以下が公式ドキュメントとその使い方です。 https://github.com/agreatfool/grpc_tools_node_protoc_ts#how-to-use # ソースコードの出力先 PROTO_DEST=./src/proto # protoファイルが置いてあるディレクトリ PROTO_DIR=./proto # Protocol BuffersからnodeのgRPCコードを自動生成 grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=${PROTO_DEST} \ --plugin=protoc-gen-grpc=$(which grpc_tools_node_protoc_plugin) \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* # typescript用の型定義を作成 grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=${PROTO_DEST} \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* もしくは以下のように一括で生成する方法もあります。 # 定義と実装を同時に生成 yarn run grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=grpc_js:${PROTO_DEST} \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=grpc_js:${PROTO_DEST} \ -I ${PROTO_DIR} \ ${PROTO_DIR}/*.proto 生成されたプログラムを利用してみる Hello サービスの簡単なサーバ、クライアントの実装を以下に示します。 通信に関わる処理は全てgRPC側が行ってくれているため、実際に記述する部分はロジックに対応する部分だけとなります。通信に用いるメッセージを組み立てる関数も自動生成されているので、それを利用して構築できます。 // サーバプログラム import * as grpc from '@grpc/grpc-js' ; import { sendUnaryData } from '@grpc/grpc-js/build/src/server-call' ; import { HelloRequest , HelloResponse } from '../proto/hello_pb' ; import { HelloService } from '../proto/hello_grpc_pb' ; const HelloServer = { hello: ( call: grpc.ServerUnaryCall < HelloRequest , HelloResponse >, callback: sendUnaryData < HelloResponse >) : void => { const request = call.request ; const response = new HelloResponse (); console .log ( "Message from client" ); response.setResult ( "Hello," + request.getName ()) callback ( null , response ); } } function serve () : void { const server = new grpc.Server (); server.addService ( HelloService , HelloServer ); server.bindAsync ( `localhost:6543` , grpc.ServerCredentials.createInsecure (), ( err , port ) => { if ( err ) { throw err ; } console .log ( `Listening on ${ port } ` ); server.start (); } ); } serve (); // クライアントプログラム import * as grpc from '@grpc/grpc-js' ; import { HelloClient } from '../proto/hello_grpc_pb' ; import { HelloRequest , HelloResponse } from '../proto/hello_pb' ; function hello () : Promise < HelloResponse > { const client = new HelloClient ( `localhost:6543` , grpc.credentials.createInsecure (), ); // HelloRequestを作るためのクラス、メソッドが用意されているのでそれを用いてメッセージを作成する const request = new HelloRequest (); request.setName ( "ISID" ); // サーバに対してサービスの実行を要求する return new Promise < HelloResponse >(( resolve , reject ) => { console .log ( "Send Hello Message" ); client.hello ( request , ( err , response ) => { if ( err ) { return reject ( err ); } // ここで結果を受け取っている console .log ( "Receive Message" ); return resolve ( response ); } ); } ); } (async () => { console .log ( "Client Start" ); const result = await hello (); console .log ( result.getResult ()); } )(); 実際のプロジェクト構成と動作例 ここでは、今回のサンプルで利用したプロジェクトの構成を紹介します。 プロジェクト構成例 ディレクト リの構成は以下のような構成です。 . ├── package.json ├── proto │   └── hello.proto ├── scripts │   └── build-protos.sh ├── src │   ├── client │   │   └── index.ts │   ├── proto │   └── server │   └── index.ts └── tsconfig.json また、package. json の中身は以下のような内容です。 { " name ": " grpc-hello ", " version ": " 1.0.0 ", " main ": " index.js ", " license ": " MIT ", " dependencies ": { } , " scripts ": { " lint ": " yarn run eslint --fix --ext .ts src ", " clean ": " rm -rf ./dist && rm -rf ./src/proto && mkdir -p ./src/proto ", " build ": " sh ./scripts/build-protos.sh ./hello.proto ./src/proto && yarn run tsc; cp -r ./src/proto ./dist/src/proto " , } , " devDependencies ": { " @grpc/grpc-js ": " ^1.4.2 ", " @types/eslint ": " ^7.28.2 ", " @types/google-protobuf ": " ^3.15.5 ", " @types/node ": " ^16.11.7 ", " @typescript-eslint/eslint-plugin ": " ^5.3.0 ", " @typescript-eslint/parser ": " ^5.3.0 ", " eslint ": " ^8.1.0 ", " grpc-tools ": " ^1.11.2 ", " grpc_tools_node_protoc_ts ": " ^5.3.2 ", " ts-node ": " ^10.4.0 ", " tsconfig-paths ": " ^3.11.0 ", " typescript ": " ^4.4.4 " } } 動作確認 まず yarn build で Protocol Buffers からコード生成、TypeScriptから JavaScript へのトランスパイルを実行します。 $ yarn $ yarn build サーバの起動 $ node ./dist/src/server/ Listening on 6543 クライアントの実行 $ node ./dist/src/client Client Start Send Hello Message Receive Message Hello,ISID サーバー側のログも確認すると Message from client というようなログが出ています。 gRPCを使った通信プログラムが実装できてそうです。 まとめ 今回は、gRPCを用いたアプリケーション開発の方法やその注意点について紹介しました。 gRPCは Protocol Buffers を定義するだけで高品質な通信プログラムが実装できる素晴しい技術ですね。 機会があれば積極的に利用していきたいと思います。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。 ソリューションアーキテクト 執筆: @yamashita.tsuyoshi 、レビュー: @handa.kenta ( Shodo で執筆されました )
はじめに ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わってる案件では、コンテナ利用、 AWS Fargate利用を進めております。 AWS Fargateのお手軽さは非常に重宝しております。 しかし、そこで問題になってくるのが、管理接続、踏み台系をどうするかです。 アプリケーション本体をせっかく AWS Fargateでやっているので、管理系接続、踏み台的な作業もできるだけ非EC2でやりたいですよね? ということで、 AWS FargateでECS Exec、session managerをどのように運用に使っていけるか紹介いたします。 目次 はじめに 目次 想定作業、目標 基本のECS Exec実行 aws ssm start-session start-session with AWS-StartPortForwardingSession start-session with AWS-StartPortForwardingSessionToRemoteHost 番外:ECS FargateでSSMセッションマネージャーのリモートホストのポートフォワード環境を構築する ECS Exec with 運用スクリプト まとめ 想定作業、目標 下記のような想定作業、目標となります。 想定作業: RDSに接続し管理作業(pgdump、pgrestore、レコードの状態確認) ECS Fagateが使ってるEFSのファイル配置、ファイル取得 トラブル時のECSの状態把握(設定ファイル、 環境変数 等) 目標: 管理用のEC2を建てたくない 筆者が関係しているアカウントは200アカウント以上あり、各アカウントに踏み台を立てるのも、集中管理踏み台で VPC ピアリングを考えるのも非常にどちらも避けたい感覚です(というか今苦しんでます) 権限管理を AWS のロールに寄せたい ec2環境では作業時にwinパスワード、 ssh キーを払い出すような仕組みを実装しているのですが(チケット管理+ssm)、トラブルが一定頻度で発生しています。また、利用者としても踏み台への接続情報(接続経路、IP)、認証情報を意識するのは煩雑です。 監査ログを AWS に寄せたい ECS Execでメンテナンスコマンドの手打ちを辞めたい ローカルから、ポート フォワ ード等で デバッグ 用のセッションを張りたい 基本のECS Exec実行 基本のECS Exec実行です。 工夫としてはタスクIDの取得が煩雑なため、環境情報の取得を自動取得にしているだけです。本取得条件は、1 クラスタ ーに1ecsサービスとなっているので、ECS クラスタ ー、サービスの状況に応じ、適宜クエリ条件は編集してください。 cl = $( aws ecs list-clusters | jq -r ' .clusterArns[0] ' ) prefix = ` echo ${cl} | sed -E ' s/.+cluster\///g ' ` taskarn = $( aws ecs list-tasks --cluster ${cl} | jq -r ' .taskArns[0] ' ) taskid = ` echo ${taskarn} | sed -E ' s/.+task\/.+\///g ' ` CONTAINER_NAME = " ${prefix} -manager-container " echo ${cl} ; \ echo ${prefix} ; \ echo ${taskarn} ; \ echo ${taskid} ; \ echo ${CONTAINER_NAME} ; \ aws ecs execute-command \ --region ap-northeast-1 \ --cluster ${cl} \ --task ${taskarn} \ --container ${CONTAINER_NAME} \ --command " /bin/sh " \ --interactive あと、ECS Execは、cloud shellから実行すれば、アクセスキー、シークレットキーいらずで、 AWS consoleアクセス、フェデレーション管理に寄せることができます。 aws ssm start-session 参考記事: https://winebarrel.hatenablog.com/entry/2021/10/23/165720 次に、対話式シェル以外でもいろいろできないか探っていきます。 上記ブログで下のようなコメントがあり、私は下のところで止まっておりました。 ECS ExecがどうもマネージドなSSM Agentを使って動いているということはわかっていたが、実際どうなっているのかよくわからなかった。 ・・・・ aws ssm start-sessionを実行できそうな雰囲気があるが、--targetに何を渡せばいいのかよくわからなかった。 が、続けて読んでいくと、 ソースコード を読めばtargetの書式が書かれてるとのこと。 https://github.com/aws/aws-cli/blob/c0edee0a7427b6e7b654df0696015e96105497a3/awscli/customizations/ecs/executecommand.py#L61-L73 def build_ssm_request_paramaters (response, client): cluster_name = response[ 'clusterArn' ].split( '/' )[- 1 ] task_id = response[ 'taskArn' ].split( '/' )[- 1 ] container_name = response[ 'containerName' ] # in order to get container run-time id # we need to make a call to describe-tasks container_runtime_id = \ get_container_runtime_id(client, container_name, task_id, cluster_name) target = "ecs:{}_{}_{}" .format(cluster_name, task_id, container_runtime_id) ssm_request_params = { "Target" : target} return ssm_request_params ソースコード を読めば確かに早かったですね。。。 ということで、 aws ssm start-sessionの各ドキュメントを試します。 まずは基本のsession接続。 cl = $( aws ecs list-clusters | jq -r ' .clusterArns[0] ' ) prefix = ` echo ${cl} | sed -E ' s/.+cluster\///g ' ` taskarn = $( aws ecs list-tasks --cluster ${cl} | jq -r ' .taskArns[] ' ) taskid = ` echo ${taskarn} | sed -E ' s/.+task\/.+\///g ' ` CONTAINER_NAME = " ${prefix} -manager-container " CONTAINER_ID = $( aws ecs describe-tasks --cluster $cl --task $taskid | jq -r --arg CONTAINER_NAME $CONTAINER_NAME ' .tasks[0].containers[] | select(.name == $CONTAINER_NAME).runtimeId ' ) echo ${cl} ; \ echo ${prefix} ; \ echo ${taskarn} ; \ echo ${taskid} ; \ echo ${CONTAINER_NAME} ; echo ${CONTAINER_ID} ; aws ssm start-session --target ecs: ${prefix} _ ${taskid} _ ${CONTAINER_ID} パラメータとしてCONTAINER_IDの取得が増えておりますが、つながりました。まあ、これだけだとecs execと同じためなんのありがたさもありませんが。 なお、 AWS Fargateでのstart-session利用は AWS CLI の ソースコード を読んで得た知見であり、 AWS がサポートしている使い方ではありません。 start-session with AWS -StartPortForwardingSession では、次に、 AWS -StartPortForwardingSessionを試していきましょう。 もしできれば、接続トラブル時の切り分け作業、インターネットに公開するのに抵抗のある管理ポートへのアクセス等、いろいろ使い道があります。 #前述のおまじない aws ssm start-session --target ecs: ${prefix} _ ${taskid} _ ${CONTAINER_ID} \ --document-name AWS-StartPortForwardingSession --parameters ' {"portNumber":["80"],"localPortNumber":["18080"]} ' ドキュメントとパラメータを追加し実行です。 ということで、つながりました。まあ、cloudshellからだと使い道がありませんが(バックグランド実行のしかたがわかりませんでした)、ローカルマシンから利用するには便利そうです。 start-session with AWS -StartPortForwardingSessionToRemoteHost では、次に、 AWS -StartPortForwardingSessionToRemoteHostを試します。 「 AWS -StartPortForwardingSessionToRemoteHost」って何?って人は、 クラスメソッド様のAWS System Managerセッションマネージャーがリモートホストのポートフォワードに対応しました をご覧ください。 ざっくりいってしまうと、session managerの対象ホストから他のホストにポート フォワ ードできる優れものです。session managerに対応していないRDSへポート フォワ ードできたりします。 これができれば、ローカルのGUIDB管理ツールからRDSへ接続が可能となります( CLI だと厳しいというメンバが一定数おりそれ用の対応検討です)。 #前述のおまじない aws ssm start-session \ --target ecs: ${prefix} _ ${taskid} _ ${CONTAINER_ID} \ --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters ' {"host":["RDSエンドポイント],"portNumber":["3306"], "localPortNumber":["3306"]} ' で、実施結果ですが、、。 2022/07、2022/09で二回ほど試しておりますが、現行の AWS Fargateに入ってるエージェントバージョンが未対応とのこと。当面、ローカルからの直接RDS接続はお預けです。 番外:ECS FargateでSSMセッションマネージャーの リモートホスト のポート フォワ ード環境を構築する ECS FargateでSSMセッションマネージャーのリモートホストのポートフォワード環境を構築する にかかれているように、Fargate基盤のエージェントを利用するのではなく、ssmエージェント設定したコンテナをfargateにデプロイすれば リモートホスト へのポート フォワ ード環境の構築は可能です。 また、上記の方法以外にも、下記のような仕込みをすることで対象コンテナへのポート フォワ ードRDS接続はできそうです。 これでコンテナに[stone](https://www.gcd.org/sengoku/stone/Welcome.ja.html)でも入れておけば、Fargateのタスクを踏み台にしてデータベースなどへのアクセスができる。 参考記事: https://winebarrel.hatenablog.com/entry/2021/10/23/165720 ECS Exec with 運用 スクリプト 前述までの内容で、ポート フォワ ード、対話式でいろいろな管理オペレーションができそうということは見えてきました。 が、やはり、定型作業は単 純化 していきたいので対話式以外でのメンテナンス方式を考えていきます。 [アップデート] 実行中のコンテナに乗り込んでコマンドを実行できる「ECS Exec」が公開されました で、上の記事を見てると下記のような気になる記述が。 aws の公式ドキュメントをみてると、 /bin/sh を呼ぶ例しか書いていないのですが、任意のコマンドで呼べるんですね。 ということで、運用 スクリプト をコンテナ内に配置し呼び出してみます。 #前述のおまじない #pgdump⇒s3アップロードをしている運用スクリプトを呼び出している例 aws ecs execute-command \ --region ap-northeast-1 \ --cluster ${cl} \ --task ${taskarn} \ --container ${CONTAINER_NAME} \ --command " /bin/sh -c '/work/backup.sh backupComment' " \ --interactive #s3ダウンロード⇒pgrestoreをしている運用スクリプトを呼び出している例 aws ecs execute-command \ --region ap-northeast-1 \ --cluster ${cl} \ --task ${taskarn} \ --container ${CONTAINER_NAME} \ --command " /bin/sh -c '/work/restore.sh my-dev_20220902-153423_backupComment.dump' " \ --interactive こんな感じで、運用 スクリプト をECS Execから呼びだすことができます(なお、重い運用JOBの場合は、専用のタスクを起動した方がよいと考えております)。 ということで、既存構成に多少の手を加えることにより、ECS Execからメンテナンスをする仕組みが実装できました。 ECS Execで呼び出せるように運用 スクリプト を定義しておけば、今後下記のようなこともできそうです。 今までどおりcloudshellから Ops が手動でECS Execにて運用 スクリプト を起動する ChatOpsでECS Exec経由で運用 スクリプト を起動する チケット管理ツールのワークフローからECS Exec経由で運用 スクリプト を起動する(一部のssm run-commandはこの方式で実施しています) まとめ ということで、 AWS Fargate利用時の運用タスクの実行方法についてご紹介してみました。 執筆: @miura.toshihiko ( Shodo で執筆されました )
はじめに ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わってる案件では、コンテナ利用、 AWS Fargate利用を進めております。 AWS Fargateのお手軽さは非常に重宝しております。 しかし、そこで問題になってくるのが、管理接続、踏み台系をどうするかです。 アプリケーション本体をせっかく AWS Fargateでやっているので、管理系接続、踏み台的な作業もできるだけ非EC2でやりたいですよね? ということで、 AWS FargateでECS Exec、session managerをどのように運用に使っていけるか紹介いたします。 目次 はじめに 目次 想定作業、目標 基本のECS Exec実行 aws ssm start-session start-session with AWS-StartPortForwardingSession start-session with AWS-StartPortForwardingSessionToRemoteHost 番外:ECS FargateでSSMセッションマネージャーのリモートホストのポートフォワード環境を構築する ECS Exec with 運用スクリプト まとめ 想定作業、目標 下記のような想定作業、目標となります。 想定作業: RDSに接続し管理作業(pgdump、pgrestore、レコードの状態確認) ECS Fagateが使ってるEFSのファイル配置、ファイル取得 トラブル時のECSの状態把握(設定ファイル、 環境変数 等) 目標: 管理用のEC2を建てたくない 筆者が関係しているアカウントは200アカウント以上あり、各アカウントに踏み台を立てるのも、集中管理踏み台で VPC ピアリングを考えるのも非常にどちらも避けたい感覚です(というか今苦しんでます) 権限管理を AWS のロールに寄せたい ec2環境では作業時にwinパスワード、 ssh キーを払い出すような仕組みを実装しているのですが(チケット管理+ssm)、トラブルが一定頻度で発生しています。また、利用者としても踏み台への接続情報(接続経路、IP)、認証情報を意識するのは煩雑です。 監査ログを AWS に寄せたい ECS Execでメンテナンスコマンドの手打ちを辞めたい ローカルから、ポート フォワ ード等で デバッグ 用のセッションを張りたい 基本のECS Exec実行 基本のECS Exec実行です。 工夫としてはタスクIDの取得が煩雑なため、環境情報の取得を自動取得にしているだけです。本取得条件は、1 クラスタ ーに1ecsサービスとなっているので、ECS クラスタ ー、サービスの状況に応じ、適宜クエリ条件は編集してください。 cl = $( aws ecs list-clusters | jq -r ' .clusterArns[0] ' ) prefix = ` echo ${cl} | sed -E ' s/.+cluster\///g ' ` taskarn = $( aws ecs list-tasks --cluster ${cl} | jq -r ' .taskArns[0] ' ) taskid = ` echo ${taskarn} | sed -E ' s/.+task\/.+\///g ' ` CONTAINER_NAME = " ${prefix} -manager-container " echo ${cl} ; \ echo ${prefix} ; \ echo ${taskarn} ; \ echo ${taskid} ; \ echo ${CONTAINER_NAME} ; \ aws ecs execute-command \ --region ap-northeast-1 \ --cluster ${cl} \ --task ${taskarn} \ --container ${CONTAINER_NAME} \ --command " /bin/sh " \ --interactive あと、ECS Execは、cloud shellから実行すれば、アクセスキー、シークレットキーいらずで、 AWS consoleアクセス、フェデレーション管理に寄せることができます。 aws ssm start-session 参考記事: https://winebarrel.hatenablog.com/entry/2021/10/23/165720 次に、対話式シェル以外でもいろいろできないか探っていきます。 上記ブログで下のようなコメントがあり、私は下のところで止まっておりました。 ECS ExecがどうもマネージドなSSM Agentを使って動いているということはわかっていたが、実際どうなっているのかよくわからなかった。 ・・・・ aws ssm start-sessionを実行できそうな雰囲気があるが、--targetに何を渡せばいいのかよくわからなかった。 が、続けて読んでいくと、 ソースコード を読めばtargetの書式が書かれてるとのこと。 https://github.com/aws/aws-cli/blob/c0edee0a7427b6e7b654df0696015e96105497a3/awscli/customizations/ecs/executecommand.py#L61-L73 def build_ssm_request_paramaters (response, client): cluster_name = response[ 'clusterArn' ].split( '/' )[- 1 ] task_id = response[ 'taskArn' ].split( '/' )[- 1 ] container_name = response[ 'containerName' ] # in order to get container run-time id # we need to make a call to describe-tasks container_runtime_id = \ get_container_runtime_id(client, container_name, task_id, cluster_name) target = "ecs:{}_{}_{}" .format(cluster_name, task_id, container_runtime_id) ssm_request_params = { "Target" : target} return ssm_request_params ソースコード を読めば確かに早かったですね。。。 ということで、 aws ssm start-sessionの各ドキュメントを試します。 まずは基本のsession接続。 cl = $( aws ecs list-clusters | jq -r ' .clusterArns[0] ' ) prefix = ` echo ${cl} | sed -E ' s/.+cluster\///g ' ` taskarn = $( aws ecs list-tasks --cluster ${cl} | jq -r ' .taskArns[] ' ) taskid = ` echo ${taskarn} | sed -E ' s/.+task\/.+\///g ' ` CONTAINER_NAME = " ${prefix} -manager-container " CONTAINER_ID = $( aws ecs describe-tasks --cluster $cl --task $taskid | jq -r --arg CONTAINER_NAME $CONTAINER_NAME ' .tasks[0].containers[] | select(.name == $CONTAINER_NAME).runtimeId ' ) echo ${cl} ; \ echo ${prefix} ; \ echo ${taskarn} ; \ echo ${taskid} ; \ echo ${CONTAINER_NAME} ; echo ${CONTAINER_ID} ; aws ssm start-session --target ecs: ${prefix} _ ${taskid} _ ${CONTAINER_ID} パラメータとしてCONTAINER_IDの取得が増えておりますが、つながりました。まあ、これだけだとecs execと同じためなんのありがたさもありませんが。 なお、 AWS Fargateでのstart-session利用は AWS CLI の ソースコード を読んで得た知見であり、 AWS がサポートしている使い方ではありません。 start-session with AWS -StartPortForwardingSession では、次に、 AWS -StartPortForwardingSessionを試していきましょう。 もしできれば、接続トラブル時の切り分け作業、インターネットに公開するのに抵抗のある管理ポートへのアクセス等、いろいろ使い道があります。 #前述のおまじない aws ssm start-session --target ecs: ${prefix} _ ${taskid} _ ${CONTAINER_ID} \ --document-name AWS-StartPortForwardingSession --parameters ' {"portNumber":["80"],"localPortNumber":["18080"]} ' ドキュメントとパラメータを追加し実行です。 ということで、つながりました。まあ、cloudshellからだと使い道がありませんが(バックグランド実行のしかたがわかりませんでした)、ローカルマシンから利用するには便利そうです。 start-session with AWS -StartPortForwardingSessionToRemoteHost では、次に、 AWS -StartPortForwardingSessionToRemoteHostを試します。 「 AWS -StartPortForwardingSessionToRemoteHost」って何?って人は、 クラスメソッド様のAWS System Managerセッションマネージャーがリモートホストのポートフォワードに対応しました をご覧ください。 ざっくりいってしまうと、session managerの対象ホストから他のホストにポート フォワ ードできる優れものです。session managerに対応していないRDSへポート フォワ ードできたりします。 これができれば、ローカルのGUIDB管理ツールからRDSへ接続が可能となります( CLI だと厳しいというメンバが一定数おりそれ用の対応検討です)。 #前述のおまじない aws ssm start-session \ --target ecs: ${prefix} _ ${taskid} _ ${CONTAINER_ID} \ --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters ' {"host":["RDSエンドポイント],"portNumber":["3306"], "localPortNumber":["3306"]} ' で、実施結果ですが、、。 2022/07、2022/09で二回ほど試しておりますが、現行の AWS Fargateに入ってるエージェントバージョンが未対応とのこと。当面、ローカルからの直接RDS接続はお預けです。 番外:ECS FargateでSSMセッションマネージャーの リモートホスト のポート フォワ ード環境を構築する ECS FargateでSSMセッションマネージャーのリモートホストのポートフォワード環境を構築する にかかれているように、Fargate基盤のエージェントを利用するのではなく、ssmエージェント設定したコンテナをfargateにデプロイすれば リモートホスト へのポート フォワ ード環境の構築は可能です。 また、上記の方法以外にも、下記のような仕込みをすることで対象コンテナへのポート フォワ ードRDS接続はできそうです。 これでコンテナに[stone](https://www.gcd.org/sengoku/stone/Welcome.ja.html)でも入れておけば、Fargateのタスクを踏み台にしてデータベースなどへのアクセスができる。 参考記事: https://winebarrel.hatenablog.com/entry/2021/10/23/165720 ECS Exec with 運用 スクリプト 前述までの内容で、ポート フォワ ード、対話式でいろいろな管理オペレーションができそうということは見えてきました。 が、やはり、定型作業は単 純化 していきたいので対話式以外でのメンテナンス方式を考えていきます。 [アップデート] 実行中のコンテナに乗り込んでコマンドを実行できる「ECS Exec」が公開されました で、上の記事を見てると下記のような気になる記述が。 aws の公式ドキュメントをみてると、 /bin/sh を呼ぶ例しか書いていないのですが、任意のコマンドで呼べるんですね。 ということで、運用 スクリプト をコンテナ内に配置し呼び出してみます。 #前述のおまじない #pgdump⇒s3アップロードをしている運用スクリプトを呼び出している例 aws ecs execute-command \ --region ap-northeast-1 \ --cluster ${cl} \ --task ${taskarn} \ --container ${CONTAINER_NAME} \ --command " /bin/sh -c '/work/backup.sh backupComment' " \ --interactive #s3ダウンロード⇒pgrestoreをしている運用スクリプトを呼び出している例 aws ecs execute-command \ --region ap-northeast-1 \ --cluster ${cl} \ --task ${taskarn} \ --container ${CONTAINER_NAME} \ --command " /bin/sh -c '/work/restore.sh my-dev_20220902-153423_backupComment.dump' " \ --interactive こんな感じで、運用 スクリプト をECS Execから呼びだすことができます(なお、重い運用JOBの場合は、専用のタスクを起動した方がよいと考えております)。 ということで、既存構成に多少の手を加えることにより、ECS Execからメンテナンスをする仕組みが実装できました。 ECS Execで呼び出せるように運用 スクリプト を定義しておけば、今後下記のようなこともできそうです。 今までどおりcloudshellから Ops が手動でECS Execにて運用 スクリプト を起動する ChatOpsでECS Exec経由で運用 スクリプト を起動する チケット管理ツールのワークフローからECS Exec経由で運用 スクリプト を起動する(一部のssm run-commandはこの方式で実施しています) まとめ ということで、 AWS Fargate利用時の運用タスクの実行方法についてご紹介してみました。 執筆: @miura.toshihiko ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 Stable Diffusionシリーズ、今回は、美少女を高確率で出す呪文です。 Stable Diffusionで人物を出力するときに、顔が崩れている、目が変、口がない、腕(手)が変などを経験した方は多いことでしょう。NSFW(職場や学校で閲覧注意)が連続で出て、心が折れそうになった方もいることでしょう。僕もそのうちの一人です。 今回は、クオリティの高い人物画を高確率で出力し、NSFWもほとんど起きない呪文を研究しました。 v2.1 美少女アニメ画 もよろしければご覧ください。 Stable Diffusionのおすすめコンテンツはこちら。 Waifu Diffusion 1.3.5_80000 v2.1 金髪美女写真 v2.1 美少女アニメ画 v2.1 AUTOMATIC1111 v2.0 美少女イラスト v1.5 美少女画検証 美少女アニメ画改善版 美少女を高確率で出す呪文編 美少女アニメ画編 美少女写真編 女性イラスト編 魅惑的な女アニメ画(トゥーンレンダリング)編 長い呪文は切り捨てられる編 illustration of a beautiful girl detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust artstation, deviantart, concept art, digital painting, award-winning looking far away, shot diagonally cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose ray tracing, 8k まとめ 仲間募集 Stable Diffusionの過去コンテンツ illustration of a beautiful girl 今回もシンプルな呪文から始めましょう。徐々に呪文を足していき、その効果を確認することが呪文を学ぶには効果的です。 今回の呪文 illustration of a beautiful girl Dream Studio などの実行環境のある方は、10回ほど試してください。クオリティがバラバラなのが確認できるでしょう。今回の呪文はシンプルすぎて、出力が安定しないのです。 出力結果 detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust 今回の研究結果で最も重要なのが、これらの呪文。人物の出力を安定させるには、detailedで、体のパーツを指定します。 armsを指定することで、腕が変になることがだいぶ減りました。 detailedを指定すると、Stable Diffusionは指定された部分をクローズアップしようとします。今回は、顔のパーツを多く指定しているため、必然的に顔がクローズアップされます。顔がクローズアップされた画像は、バリエーションが出しにくいため、単調なものになりがちです。それを少しでも緩和するために、bust(胸)を指定しています。 detailed waist(腰)も指定してみましたが、あまり効果がないのと、NSFWが起きる確率が増えるため、呪文にいれませんでした。 detailed legs(脚)も指定してみましたが、NSFWが頻繁に起きるので、呪文にいれませんでした。 detailed body(体)も指定してみましたが、効果が感じられなかったので、呪文にいれませんでした。 detailed skin(肌)も指定してみましたが、ときどきノイズがのるので、呪文にいれませんでした。 detailed eyelashes(まつげ)も指定してみましたが、ときどきノイズがのる(目の下にもまつげが出たりする)ので、呪文にいれませんでした。 detailed eyebrows(眉毛)も指定してみましたが、不自然に強調されているように感じることがあったので、呪文にいれませんでした。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust Dream Studio などの実行環境のある方は、10回ほど試してください。変な画像が出力されることはなくなったと思います。ただし、クオリティは安定してませんね。 出力結果 artstation, deviantart , concept art, digital painting, award-winning クオリティを安定させるためには、作風を指定することが重要です。イラストの投稿サイトとして、artstation, deviantart を指定します。concept art, digital painting, award-winningは、お決まりの呪文です。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust, artstation, deviantart, concept art, digital painting, award-winning 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust, artstation, deviantart, concept art, digital painting, award-winning Dream Studio などの実行環境のある方は、10回ほど試してください。クオリティも安定してきましたね。 出力結果 looking far away, shot diagonally すでに人物画のクオリティは、かなり上がっていますが、正面を見ている構図が多く、単調になりがちです。目線を正面から外してみましょう。looking far away(遠くを見る)を指定します。 shot diagonally(斜めから撮る)で、撮影の角度に変化をつけましょう。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning 出力結果 だいぶ雰囲気が出てきたのではないでしょうか。 cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose 画像のクオリティを上げるには、シーン(scene)、構図(composition)、ライティング(lighting)を指定することが重要です。魔法の呪文、cinematicをつけておきましょう。 cinematic postprocessing, overexposeでさらにクオリティを向上させます。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed beautiful eyes, detailed mouth, detailed arms, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed beautiful eyes, detailed mouth, detailed arms, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose 出力結果 キラキラして映画の1シーンのようですね。 ただし、この呪文から、再現度は低くなります。いつも映画のような1シーンになるわけではないということです。 ray tracing, 8k 先程の呪文で完成にしてもよいのですが、もう少し、フォトリアルにしたい場合は、ray tracing, 8kの呪文を追加します。 今回の呪文(横長、コピー&ペースト用) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k 閲覧用呪文(改行版) illustration of a beautiful girl, detailed beautiful face, detailed hair, detailed human eyes, detailed mouth, detailed arms, detailed bust, looking far away, shot diagonally, artstation, deviantart, concept art, digital painting, award-winning, cinematic postprocessing, cinematic scene, cinematic composition, cinematic lighting, overexpose, ray tracing, 8k 出力結果 まとめ 今回は、クオリティの高い人物画を高確率で出力する方法を紹介しました。 顔がクローズアップされ、正面を向いている画像は、ありきたりな感じになるリスクがあります。変化をつける呪文を学びましょう。 次回は、 長い呪文は切り捨てられる編 です。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア Stable Diffusionの過去コンテンツ 人物写真編 レンズ編 画像タイプ編 美少女アニメ画編 美少女写真編 女性イラスト編 美しい夜空を見渡す男編 魅惑的な女アニメ画(トゥーンレンダリング)編 美少女を高確率で出す呪文編 長い呪文は切り捨てられる編 蒸気機関が高度に発達したレトロなアニメ(スチームパンク)の世界観編 A as Bの呪文による画像合成編 かわいい動物の擬人化編 バベルの塔のイラスト編 TPU版の使い方 美少女アニメ画改善版 v1.5 美少女画検証 東京タワーの写真 折り紙合体変形ロボ v2.0 美少女イラスト v2.1 AUTOMATIC1111 v2.1 美少女アニメ画 v2.1 金髪美女写真 Waifu Diffusion 1.3.5_80000 執筆: @higa ( Shodo で執筆されました )