default eye-catch image.

SnowflakeのTime Travel

SnowPro Coreの頻出テーマだと感じたTime Travel。 資格取得時に固め打ちした記憶があるが、補強ついでにもう少し詳し目に公式を読んでみる。 古くなったり間違っていたりするかもしれないので、事の真偽については公式を参照のこと。 Time Travelの理解と使用 https://docs.snowflake.com/ja/user-guide/data-time-travel.html [arst_toc tag=\"h4\"] Time Travelとは 通常、データ削除後に削除したデータにアクセスするには削除前にデータのバックアップが必要。 バックアップしてリストアして、というのはある意味DB製品の基本的な動作仕様であって、 SnowflakeにもSnowflakeのフルマネージドなポリシーに基づいて仕組みが用意されている。 Snowflakeではデータが自動的・透過的にバックアップされ、 明示的にバックアップ・リストアせずに削除後に削除前のデータにアクセスできる。 何も気にしないでも裏で勝手にバックアップ・削除されるため大分手間が省略される。 当然ストレージコストを余分に消費するが保持期間を設定することでバランスを制御できる。 公式には以下の用途で使われる、と書いてある。 誤って削除したデータの復元 特定時点の復元 任意期間の使用量・操作の分析 データのライフサイクル 重要な観点として、データにはステートがあり、ライフサイクルが決まっている。 ステート 削除種別 用途 通常 - 現在のデータに対するクエリ、DDL、DML、など Time Travel 論理削除 更新・削除された過去のデータへのクエリ過去の特定の時点についてテーブル・スキーマ・DB全体のクローン削除されたテーブル・スキーマ・DBの復元 Fail-safe 物理削除 一定期間(Retantion Period)が過ぎるとデータはFail-safeに移動。操作不可。Snowflakeへ問い合わせて何とかなる可能性がある データの保持期間(Retentiono period) ユーザはデータの保持期間を変更できる。 保持期間は日単位で設定する。デフォルト値は1(24時間)。 ゼロを設定するとTime Travelを使用しない設定。 設定範囲はテーブル種別、Snowflakeのエディションによって異なる。 通常のテーブルについて、エディションごとの設定範囲は以下の通り。 (Temporaryテーブル,Transientテーブルは通常1日を超えて使わないはずなので以下では除外) エディション 0(Time Travelを使用しない) 1日 〜90日 Standard 可 可(デフォルト) 不可 Enterprise+ 可 可(デフォルト) 可 さらに、ACCOUNTADMINロールを持つユーザはユーザの設定範囲を限定できる。 デフォルト値は DATA_RETENTION_TIME_IN_DAYS、 最小値は MIN_DATA_RETENTION_TIME_IN_DAYS。 最小値設定はデフォルト値設定を上書きしない。 デフォルト値が最小値よりも小さい場合、いずれかの大きい方が適用される。 コスト Time Travelは論理削除のステートでありストレージコストがかかる。 データが変更された時点から1日ごとに課金。(ちょっと詳細不明...) テーブルを丸ごとDROPした場合には丸ごと保存されるがなるべく差分が保存される。 ETLなどに使う1日未満のデータはTransientテーブルに格納することになっている。 また、より短いセッション内で使うデータはTemporaryテーブルに格納することになっている。 そのような用途であれば長いTime Travelは不要だし、そもそもFail-safeも不要。 これらのテーブル種別については、Time Travel期間は最大1日となっていて、 さらに後続のステートであるFail-safeに遷移しない。 逆に言うと、Transient,Temporaryテーブルを使うことでTime TravelとFail-safeの 余分なコストを最大1日に抑えることができる。 ちなみにTemporaryテーブルについてはセッションを落としたときにテーブルが破棄されると、 Time Travelの保持期間も終了する。 行ったり来たりだが、Transient,TemporaryについてはFail-safeが無いので Time Travel終了後は完全にアクセス不能となる。 保持期間の変更 テーブルの保持期間を変更すると、現在のデータとTime Travelにある全てのデータに影響する。 変更 影響 保持期間の延長 現在Time Travelにあるデータの保持期間が長くなる。例えば保持期間=10を保持期間=20に変更した場合現在Time Travel3日目のデータの残り期間は7日から17日に伸びる。 保持期間の短縮 現在アクティブなデータには新しい保持期間が適用される。例えば保持期間=10を保持期間=5に変更した場合現在Time Travel 7日目のデータはFail-safeへ遷移。現在Time Travel3日目のデータの残り期間は7日から2日に変わる。 データライフサイクルの遷移はバックグラウンドで非同期に行われるため、 ALTERコマンドで保持期間を変更したとしてすぐに上記の更新が走るわけではない。 オブジェクト階層に対する再帰的な影響 オブジェクトはCompositeパターンに基づき所有関係を持っているが、 階層上、上位のオブジェクトに対する保持期間の変更は再帰的に下位のオブジェクトに反映される。 例えばDBに対する変更はスキーマに対して反映されるなど。 ワイルドカードを使った破壊的な変更は意図しない変更をもたらすため、慎重にやったほうが良い。 最上位のアカウントに対する保持期間の変更は推奨しないという記述がある。 上位オブジェクトのドロップと下位オブジェクトの保持期間 上位オブジェクトをドロップすることで自動的に下位オブジェクトがドロップされる。 その際、下位オブジェクトの保持期間は強制的にドロップした上位オブジェクトの保持期間が設定される。 例えば保持期間10日のデータベースをドロップしたとして、 保持期間15日のスキーマ、テーブルの保持期間は強制的に10日となる。 下位オブジェクトを先にドロップすれば、下位オブジェクトの保持期間が上書きされることはない。 Time Travel中のデータに対するクエリ Time Travel中のデータにアクセスするために特別なストレージにアクセスする、という感じではなく、 SQLの拡張構文が用意され、自然にアクティブなデータとTime Travel中のデータの触り分けができる。 at句とbefore句が用意されている。 例えば公式に書かれている以下のような感じ。 ---at句によりtimestampで指定された時点の履歴データを取得 select * from my_table at (timestamp => \'Fri, 01 May 2015 16:20:00 -0700\'::timestamp_tz) ; ---5分前の時点で履歴データを取得 select * from my_table at (offset => -60*5) ; ---指定されたステートメントによる変更を含まないで、それ以前の履歴データを取得 select * from my_table before (statement => \'8e5d0ca9-005e-44e6-b858-a8f5b37c5726\') ; at句、before句が保持期間外を指す場合、クエリは失敗する。 Time Travel中のオブジェクトのクローン SQLの拡張構文によってTime Travel中のDBやスキーマなどのオブジェクトをクローンできる。 CREATEと共にCLONEを使う。例えば公式に書かれている以下のような感じ。 ---指定されたタイムスタンプで表される日付と時刻のテーブルのクローンを作成 ---my_tableというテーブルをrestored_tableというテーブルにクローン create table restored_table clone my_table at (timestamp => \'Sat, 09 May 2015 01:01:00 +0300\'::timestamp_tz) ; ---現在時刻の1時間前に存在していたスキーマと配下の全てのオブジェクトをクローン create schema restored_schema clone my_schema at (offset => -3600) ; ---指定されたステートメントの完了前に存在していたデータベースと配下の全てのオブジェクトを復元 create database        restored_db clone        my_db before       (statement => \'8e5d0ca9-005e-44e6-b858-a8f5b37c5726\'); CLONEも、指定したオブジェクトの保持期間を超えてTime Travel時間を指定するとエラーとなる。 オブジェクトのドロップと復元 オブジェクトの履歴はオブジェクトに紐づく、という書き方が正しいかは不明だが、 オブジェクト自体をドロップした場合の履歴は、オブジェクト配下の変更・削除の履歴とは少し異なる。 Time Travelは通常差分を履歴として残すが、オブジェクトのドロップによって完全な履歴が残る、 と公式に記述がある。 DROPによってオブジェクトをドロップした後、UNDROPによってドロップしたオブジェクトを復元する。 DROPした後、CREATEしたとしてもUNDROP扱いにはならないし、DROPした古いオブジェクトは残る。 永遠に完全な履歴のhistoryが積み重なっていく、historyのある時点のオブジェクトを対象に UNDROPする、という扱いとなる。 UNDROPにより復元するテーブルと同名称のテーブルが存在する場合エラーとなる。 --- mytableという名前のテーブルをdrop drop table mytable ; --- mytableという名前のテーブルをundrop undrop table mytable ; オブジェクトのhistoryについてもSQLの拡張構文で確認できる。 showとhistoryを合わせて使用する。公式は以下の通り。 オブジェクトの保持期間がすぎてTime Travelから消えるとshow historyで表示されなくなる。 --- mytestdb.myschemaスキーマ配下にあるloadから始まるテーブル名の履歴を表示 show tables history like \'load%\' in mytestdb.myschema ; --- mytestdbデータベース配下のスキーマの履歴を表示 show schemas history in mytestdb ; まとめ 自力でバックアップ・リストア操作なしで、Snowflakeが勝手にオブジェクトをバックアップしてくれる。 SQLの拡張構文を通してアクティブなデータと似た形でオブジェクトをリストアできる。 Time Travelに保持される期間はカスタマイズできる。 みたいなことについて、公式ドキュメントを読んで確認してみた。

default eye-catch image.

SnowflakeのMaterialized View

以前SnowPro core Certificationsに合格したもののなかなか使う機会がなくて、 資格試験対策レベルの薄い知識の維持すら怪しくなってきた。 Materialized Viewについて良くわからず使っていたので、 「やりなおし」のついでに知識をアップデートしていこうと思う記事第2弾。 個人の学び以上でも以下でもなく、内容に誤りがあるかもしれないので、 ことの真偽は公式ドキュメントを参照のこと。 [arst_toc tag=\"h4\"] Materialized Viewとは 何らかの集計を行おうとすると、多くの場合、中間の集計を合わせて最終的な集計結果を得る。 中間の集計を行う際にJOINにより結合を行う場合、それが高コストだと最終的にコスト高になる。 途中の集計結果をどこかに保存できれば、毎回高コストな集計を無駄に実行しなくて良くなる。 そんな時に使うのが Materialized View。「マテビュー」とか省略される。 e-Wordsによると以下の通り。 マテリアライズドビューとは、リレーショナルデータベースで作成されたビューにある程度の永続性を持たせ、参照する度に再検索しなくていいようにしたもの。特定のビューを頻繁に参照する場合に性能が向上する。 SnowflakeにおけるMaterialized Viewについては以下。 マテリアライズドビューの使用 Materialized Viewは透過的にリフレッシュされる 重要な点として、SnowflakeにおけるMaterialized Viewは自動的・透過的にリフレッシュされる。 オリジンとなるデータが変わった場合、アプリケーション側はノータッチでSnowflakeが自動更新する。 アプリケーション側でオリジンの鮮度を意識しないで良いというのはかなり楽。 透過的な自動リフレッシュの機構について、より詳細な内部情報として以下の通り。 ギリギリまでDMLを反映しないでくれる機構がついているっぽい。 クエリの前に実行されたDMLがクエリに影響する場合、クエリ時にDML反映 クエリの前に実行されたDMLがクエリに影響しない場合、スルーで応答 アプリケーション側がノータッチで自動的・透過的にリフレッシュがかかるが、 そのリフレッシュでクレジットが消費される。 普通のViewを使うべきか、Materialized Viewを使うべきか どのようなクエリであればその結果をMaterialized Viewに乗せるべきか。 クエリに時間がかかるなら使うべき。ただしオリジンとなるデータが変われば、 Materialized Viewのリフレッシュが必要となるから、オリジンが頻繁に変わるケースは対象外。 時間がかかる処理として公式には以下の例があがっている。 半構造化データに対するクエリ S3上のファイルなど遅い外部テーブルに対するクエリ 普通のViewにすべきか、Materialized Viewにすべきかの判断基準は以下。 普通のView Materialized View ビューからのクエリ結果(※) 頻繁に更新される ほとんど更新されない クエリ結果の使用 頻繁に使用される あまり使用されない リフレッシュにかかるコスト 処理時間大、ストレージ大 処理時間小、ストレージ小 ※ベースとなるテーブルが「完全に更新されない」まで限定しなくても、「クエリ結果の範囲に限定して更新されない」でOK。 Materialized Viewのパフォーマンス Snowflakeのパフォーマンスを上げる機構として「クエリ結果キャッシュ」がある。 要は同じ条件のクエリに対して、キャッシュがあればクエリを実行せずにキャッシュを返す、というもの。 実際に運用してみると「クエリ結果キャッシュ」を使わせるためには複数の条件があり、 なかなか仕様通りキャッシュを使い続けるのは難しいが、キャッシュが効けば速くなる。 比較することに意味があるのかちょっと怪しいが、 純粋にパフォーマンスを比較すると以下となるらしい。 普通のTable = 普通のView < Materialized View < クエリ結果キャッシュ クエリオプティマイザとMaterialized View アプリケーションがexplicitにMaterialized Viewを指定してやらなくても、 ベーステーブルに対するクエリ結果の行と列がMaterialized Viewに全て含まれている場合、 クエリオプティマイザが自動的にクエリを置換する。 さらに、もしベーステーブルがフィールドによってクラスタ化されていて、 プルーニングが良い結果をもたらすと判断されれば、Materialized Viewではなく ベーステーブルに対するプルーニングが使用される。 BIのようにアプリケーション全域で検索キーが分散し「どう呼ばれるかわからない」ケースでは、 なかなかカスタムでクラスタキーを設計してより良いプルーニング結果を得ることが難しいが、 分析タスクのように「ある程度呼ばれ方が決まっている」ケースならクラスタキーを偏らせる メリットはありそうで、そんなときにMaterialized Viewとベーステーブルのプルーニング、 どちらが良いか、なんてことを考えることもあるんだろう(本当か?) ただ公式には、ベストプラクティスとしてMaterialized Viewを作る場合、 ベーステーブルのプルーニングを解除しMaterialized Viewを優先すべきと記述がある。 マテリアライズドビューとそのベーステーブルをクラスタリングするためのベストプラクティス サブクエリについて暗黙的にMaterialized Viewが使われる、というケースもある。 この場合、クエリプロファイルにシレッとMaterialized Viewが鎮座する、ということになる。 Materialized Viewのメンテナンスコスト コンピューティング、ストレージともにクレジットを使用する。 Materialized Viewに対するクエリ結果が保存され、そのストレージに対してコストがかかる。 頻繁に使われるクエリなのであれば、相対的にストレージのコストが低くなると考えられるが、 もしロクに使われないクエリなのであれば、そのストレージコストが本当に低いのか考えるところ。 透過的なリフレッシュのためにコンピューティングのコストがかかる。 Materialized Viewの上手い使い方 ベーステーブルの多くデータを取得してしまうと、無駄にリソースを使ってしまう。 なるべく行・列が少なくなるようなデータセットをMaterialized Viewに格納すべき。 公式には、ベーステーブルにログがあるとして異常値のみをMaterialized Viewに置く、 という例が示されている。 要はアプリケーションの設計段階で、何をMaterialized Viewとすべきかを検討すべき。 ベーステーブルを頻繁に更新してしまうと、都度自動リフレッシュが実行されてしまう。 そのため、ベーステーブルの更新頻度を下げる必要がある。 SnowflakeはDMLをバッチ処理するように推奨している。 バッチ処理できるようなデータの並びとなるようにしておく必要がある。 DDL,DML 実際にMaterialized Viewを作ってみる。 --- ベーステーブルの作成 create table mv_base (id integer, value integer) ; --- Materialized Viewの作成 create or replace materialized view mv1 as select id, value from mv_base ; ---ベーステーブルにInsert insert into mv_base (id, value) values (1, 100) ; --- Materialized Viewからデータ取得 select id, value from mv1 ; id value --------------- 1 100 ---ベーステーブルにInsert insert into mv_base (id, value) values ( 2,200) ; --- Materialized Viewからデータ取得 select id, value from mv1 ; id value --------------- 1 100 2. 200 Materialized Viewの自動更新を停止することができる。 停止中にデータを取得しようとするとエラーが発生し取得できない。 停止と再開の組みは以下。 alter materialized view mv1 suspend ; select id, value from mv1 ; SQLコンパイルエラー: ビュー「MV1」の展開中の失敗: SQLコンパイルエラー:マテリアライズドビュー MV1 は無効です。 alter materialized view mv1 resume ; select id, value from mv1 ; id value --------------- 1 100 2. 200 まとめ 更新頻度が低く利用頻度が高い中間クエリについて Materialized View に格納すると効果的。 Materialized Viewは自動的・透過的にリフレッシュがかかる。 自動リフレッシュに際してコンピューティングコストがかかるため更新頻度は低い方が良い。 ベーステーブルに対するDMLをバッチ処理とすると自動リフレッシュの頻度を下げられる。 クエリ結果キャッシュよりは遅いが普通のテーブルよりは速い。

default eye-catch image.

Snowflakeのアクセス制御

以前SnowPro core Certificationsに合格したもののなかなか使う機会がなくて、 資格試験対策レベルの薄い知識の維持すら怪しくなってきた。 資格を取得してからかなり経過したこともあり、控えめにいって知識が陳腐化してしまった。 せっかくなので「やりなおし」のついでに知識をアップデートしていこうと思う。 セキュリティ周りについて正直よくわからず操作している感があるため、 今一度ドキュメントを見直してみる。 個人の学び以上でも以下でもなく、内容に誤りがあるかもしれないので、 ことの真偽は公式ドキュメントを参照のこと。 Snowflakeのアクセス制御 https://docs.snowflake.com/ja/user-guide/security-access-control.html [arst_toc tag=\"h4\"] アカウントの管理 例えば以下のように、オブジェクトへのアクセスを制御する。 誰がどのオブジェクトにアクセスできるのか そのオブジェクトに対してどの操作を実行できるのか 誰がアクセス制御ポリシーを作成または変更できるか アクセス制御フレームワーク 一言で「アクセス権」と言ったところで、確かに世の中には様々な意味をもって使われている。 以下、DAC,MAC,RBACの一般的なまとめ。 SnwoflakeはDACとRBACの両方に基づいてアクセス制御をおこなう。 名称 誰が制御するか 説明 任意アクセス制御DAC:Discretionary Access Control 所有者 オブジェクトには所有者がいて所有者が他者に対してオブジェクトへのアクセスを許可する例えばLinuxのファイルパーミッション。POSIXのACL。実質的に作成したリソースに対するアクセス制御の権限を与えられている。ユーザの自由度が高く管理者に手間をかけない。ルールの統一が難しく、セキュリティ面で効果を期待できない。 強制アクセス制御MAC:Mandatory Access Control 管理者 管理者がアクセスする側(サブジェクト)とされる側(オブジェクト)の両方に対してセキュリティレベルを設定する。例えばレベル1のサブジェクトはレベル 3のオブジェクトにアクセスできない等。所有者であろうとも管理者が定めた規則によりアクセスできないなどの特徴。 ロールベースアクセス制御RBAC:Role-based access control 管理者 セキュリティ概念としてはDACとMACの中間。DACと同様にサブジェクトとオブジェクトに対するアクセス制御を行うが、サブジェクトに対して「ロール」を設定し「ロール」の範囲で自由にオブジェクトにアクセスできる。つまり1つ1つのサブジェクトに個別にアクセス制御をかけるだけではなく複数のサブジェクトにアクセス制御をかける。「組織」、「部署」に親和性が高い。それぞれの部署向けにロールを作成し、部署に属したサブジェクトがロールの範囲でオブジェクトにアクセスできる。 DACとRBACの両方、とはいったいどういうことか。 まずRBACの側面から説明できるアクセス制御は以下の通り。 オブジェクトにアクセスする能力を「権限」と呼ぶ。「権限」を「ロール」に付与する。 「ロール」を他の「ロール」に割り当てたり、「ユーザ」に割り当てる。 こうして「ユーザ」は「ロール」の範囲でオブジェクトにアクセスできる。 ここまでで、「ユーザ」が「ロール」を介してオブジェクトにアクセスできる構造となる。 これだけだとRBACの説明の通り、管理者によってのみアクセス制御がおこなわれ「ユーザ」はおこなわない。 さらに、オブジェクトには「所有者」がいる。 オブジェクトを作成すると、「所有」という名前の「権限」ができる。 「所有者」には「所有権限」が割り当たった「ロール」が紐づく。 「所有権限」がある「ロール」を持っていると、オブジェクトに対する権限をロールにGRANT,REVOKEできる。 「所有権限」がある「ロール」を他に移すこともできる。 通常「所有者」にはオブジェクトに対する全ての権限が与えられる。 つまり、「所有者」であれば、DACのように「ユーザ」がアクセス制御を書き換えることができる。 DACをRBACで実装している、といった感じ。 基本的にはRBACだが、所有者に限りロールを変更できる自由さがある。 オブジェクトの階層構造と所有 SnowflakeにおいてオブジェクトはOOPでいうCompositeパターンに従う。 上位オブジェクトは下位オブジェクトのコンテナとなり、全体として階層構造を形成する。 例えば「組織」は「アカウント」を所有できるし、「アカウント」は「ユーザ」を所有できる。 オブジェクトには、オブジェクトに対してSQLを実行する権限があったりする。 例えばvwhにはSQLを実行する権限があり、もしその権限を付与されたロールをもっていれば、vwhでSQLを実行できる。 またテーブルにデータを追加する権限が付与されたロールをもっていれば、テーブルにデータを追加できる。 システム定義ロール RBACとDACを両立する上でその境界にある概念の解釈が微妙なものがあったりする。 DACにより所有者は所有するオブジェクトに関して生殺与奪の権を持つことになっているが、 いくつかのロールについてはお上が決めたルールに逆らえない。 このようなロールを「システム定義ロール」と呼び、所有者が放棄できないし、ロールから権限を無くせない。 ロール名 説明 ORGADMIN 組織レベルで運用を管理するためのロール。組織内にアカウントを作成する、組織内の全アカウント表示、組織全体の使用状況などの表示。 ACCOUNTADMIN SYSADMINとSECURITYADMINの2つをラップするロール。システムにおける最上位のロール。アカウント内の限られた数のユーザにのみ付与。 USERADMIN ユーザ、ロールの管理ができる。CREATE USER、CREATE ROLEの権限が付与されている。アカウントにユーザ、ロールを作成できる。 SECURITYADMIN USERADMINロールがSECURITYADMINロールに付与されている。USERADMINに加え、オブジェクトへのアクセス権を付与する権利が与えられている。 SYSADMIN アカウントでウェアハウス、データベースを作成する権限が与えられている。システム管理者に付与する。間違ってACCOUNTADMINロールをシステム管理者に付与しないこと。 PUBLIC 全てのユーザー、ロールにデフォルトで割り当てられるロール。PUBLICロールはオブジェクトを所有できる。全てのユーザに割り当てられているため、全てのユーザがPUBLICロールが所有するオブジェクトにアクセスできる。明示的なアクセス制御が不要で誰でも触れてよいオブジェクトをPUBLICに所有させる。 カスタムロール USERADMINロールを付与されているユーザによって、 オブジェクトを所有するロールを新たに作成できる。 ただし、RBACによってDACを実現している都合上、Snowflakeの掟に従ってロールを作るべき。 RBACベースのDACにおいて、「システム管理者」だとか「ユーザのレベル」はあくまでも「上位のロール」を付与されているか、でしか決まらない。 システム内のオブジェクトの所有者として機能するロールを作成する場合、 RBACベースのDACに配慮しないと、 「システム管理者」ですら触れない謎のオブジェクトを作り出してしまう。 「システム管理者」は、アカウント内のお全てのオブジェクトを表示、管理できるようにしたい。 もしSYSADMINロールにカスタムロールが割り当てられていないなら、 システム管理者はそのカスタムロールが所有するオブジェクトを表示、管理できない。 SECURITYADMINロールのみが表示し管理できる、という謎の状況になってしまう。 だから、新たに作成するロールは必ずSYSADMINロールに付与する必要がある。 ロールの所有関係は階層構造を持てるから、階層を上に辿ると必ずSYSADMINがある必要がある。 推奨されるロールの階層構造 Snowflakeが推奨するロールの階層構造は以下のような感じ。 矢印は「付与関係」。矢印の先のロールに、矢印の元のロールが付与されている。 ACCOUNTADMINには全てのロールが間接的に付与された状態とすること。 カスタムロールはSYSADMINに間接的に付与された状態とすること。 カスタムロールをSYSADMINを超えて直接ACCOUNTADMINに付与しないこと。 カスタムロールをSECURITYADMIN、USERADMINに付与しないこと。 最初にハマるダメケースとベストプラクティス 最上位のロールであるACCOUNTADMINを割り当てるユーザは組織内で限定するべき。 逆に言うと、もし1人しかいないACCOUNTADMINロールを持つユーザがDBを作ってしまったならば、 他のACCOUNTADMINロールを持たないユーザがそのDBを表示・管理することはできない。 ベストプラクティスは、オブジェクトの所有者となるロールをSYSADMIN配下にぶら下げること。 SYSADMINロールが付与されたユーザであれば、オブジェクトの表示・管理ができる状態とすること。 テクニックとしてオブジェクトの所有権とオブジェクトに対する権限を分ける手法があり、 もし所有者となるロールに全ての権限を付与すれば、そのロールさえユーザに割り当てれば、 ユーザはオブジェクトに対する表示・管理ができるようになる。 一方、所有者となるロールに異なるロールを割り当て、所有者となるロールには権限A、 下位のロールには権限Bを与える、という構成とすることもできる。 その際、所有者となるロールには権限A,権限Bが付与された状態となる。 SYSADMINの下に複数の管理ロールをぶら下げ、 それら全てのロールに共通して権限を付与したい、という場合には、 それぞれの管理ロールに対して、その共通の権限を付与したロールを割り当てれば良い。 この状態にするためにロールを使い分ける。 USERADMINロールを付与されたユーザがユーザとそのユーザ用のロールを作成する SYSADMINロールを付与されたユーザがオブジェクトを作成する SECURITYADMINロールを付与されたユーザが新たなユーザのロールに所有権を移動する まとめ SnowflakeのセキュリティはRBACベースのDAC。 基本的にはRBACなので管理者がロールを作って割り当てる。 ただDACを実現するためにロールがオブジェクトを所有するという概念が導入されていて、 RBACとDACの境界に解釈が難しい部分がある。Snowflakeが定めるベストプラクティスに従うと良い。 ACCOUNTADMINでDBを作りまくって他人から見えない問題をシュッと解決できる。

default eye-catch image.

AWS SAM CLIを使ってローカルでLambda関数をビルド・実行・デプロイする

Lambdaで何かをするときチマチマAWSコンソールを触らないといけないとなると面倒すぎる。 ローカルでデバッグ・デプロイできるとかなり楽になる。AWS SAMを使ってみる。 AWS SAM(Serverless Application Model)。広くAWSのServerlessサービスがまとめられている。 AWS SAM CLIのGAは2020年8月。それから何回かアップデートされている。 AWS SAMの実体はCloudFormation。CloudFormationを使ってリソースの構築が走る。 普段CloudFormationを使っていないとSAMのコマンドがコケた時に意味不明なエラーで悩むことになる。 で、悩みながらHelloWorldしてみた。 [arst_toc tag=\"h4\"] Permissions CloudFormationで各種リソースを作る仕組みであるため、同等のPermission設定が必要。 https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-permissions.html AWS SAM は、AWS リソースへのアクセスを制御するために、AWS CloudFormation と同じメカニズムを使用します。詳細については、AWS CloudFormation ユーザーガイドの「Controlling access with AWS Identity and Access Management」を参照してください。 サーバーレスアプリケーションを管理するためのユーザー権限の付与には、3 つの主なオプションがあります。各オプションは、ユーザーに異なるレベルのアクセスコントロールを提供します。 - 管理者権限を付与する。 - 必要な AWS 管理ポリシーをアタッチする。 - 特定の AWS Identity and Access Management (IAM) 許可を付与する。 必要な管理ポリシーは以下。 AWSCloudFormationFullAccess IAMFullAccess AWSLambda_FullAccess AmazonAPIGatewayAdministrator AmazonS3FullAccess AmazonEC2ContainerRegistryFullAccess 触るユーザーにロールを割り当て、上記の管理ポリシーをアタッチしておくこと。 aws configureでprofileを設定しておいて、samコマンドのオプションにprofileを渡せる。 インストール homebrewでインストール。 $ brew tap aws/tap $ brew install aws-sam-cli $ sam --version SAM CLI, version 1.37.0 初期化 sam initでプロジェクトディレクトリを作成できる。 対話的に雛形を作るか、またはテンプレートを読み込む。 Lambdaで使える言語は割と多いが、NodejsとPythonがほとんどとのこと。 NodejsがMost popular runtimeとして扱われてるんだな。 Python書きたくないなというか。all right $ mkdir samtest && cd samtest $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 Cloning from https://github.com/aws/aws-sam-cli-app-templates Choose an AWS Quick Start application template 1 - Hello World Example 2 - Multi-step workflow 3 - Serverless API 4 - Scheduled task 5 - Standalone function 6 - Data processing 7 - Infrastructure event management 8 - Machine Learning Template: 1 Use the most popular runtime and package type? (Nodejs and zip) [y/N]: y Project name [sam-app]: ----------------------- Generating application: ----------------------- Name: sam-app Runtime: nodejs14.x Architectures: x86_64 Dependency Manager: npm Application Template: hello-world Output Directory: . Next steps can be found in the README file at ./sam-app/README.md プロジェクト内は以下のような構成となった。 sam-app/ │ .gitignore │ README.md │ template.yaml ├─events │ event.json └─hello-world │ .npmignore │ app.js │ package.json └─tests └─unit test-handler.js app.jsがコード本体. Hello World.が書かれている。 eventを受け取るlambdaHandlerというアロー関数があって200を返してる。 // const axios = require(\'axios\') // const url = \'http://checkip.amazonaws.com/\'; let response; /** * * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format * @param {Object} event - API Gateway Lambda Proxy Input Format * * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html * @param {Object} context * * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html * @returns {Object} object - API Gateway Lambda Proxy Output Format * */ exports.lambdaHandler = async (event, context) => { try { // const ret = await axios(url); response = { \'statusCode\': 200, \'body\': JSON.stringify({ message: \'hello world\', // location: ret.data.trim() }) } } catch (err) { console.log(err); return err; } return response }; ビルド そもそもどういう仕組みなのかというと、Lambdaの実行環境をエミュレートしたコンテナが背後にあり、 その中でコードを実行する、ということになっている。それがゴニョゴニョと隠蔽されている。 Lambda関数のコードをビルドしてデプロイ用の「アーティファクト」を作る。 $ sam build Building codeuri: /Users/ikuty/ikuty/samtest/sam-app/hello-world runtime: nodejs14.x metadata: {} architecture: x86_64 functions: [\'HelloWorldFunction\'] Running NodejsNpmBuilder:NpmPack Running NodejsNpmBuilder:CopyNpmrc Running NodejsNpmBuilder:CopySource Running NodejsNpmBuilder:NpmInstall Running NodejsNpmBuilder:CleanUpNpmrc Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch [*] Deploy: sam deploy --guided ローカルで実行 そしてローカルで実行。 Lambdaをエミュレートするコンテナが動いてapp.jsにあるアロー関数が評価される。 1発目は重いが2発目以降は結構速い。 $ sam local invoke Invoking app.lambdaHandler (nodejs14.x) Image was not found. Removing rapid images for repo public.ecr.aws/sam/emulation-nodejs14.x Building image..................................................................................................................................................................................................................................................................................................................................................................................................................... Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.37.0-x86_64. Mounting /Users/ikuty/ikuty/samtest/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container START RequestId: e0bbec88-dafd-4e3c-8b5e-5fcb0f38f1fa Version: $LATEST END RequestId: e0bbec88-dafd-4e3c-8b5e-5fcb0f38f1fa REPORT RequestId: e0bbec88-dafd-4e3c-8b5e-5fcb0f38f1fa Init Duration: 0.47 ms Duration: 195.40 ms Billed Duration: 196 ms Memory Size: 128 MB Max Memory Used: 128 MB {\"statusCode\":200,\"body\":\"{\"message\":\"hello world\"}\"}⏎ デプロイ 以前はもっと面倒だったらしい。新しいSAMではコマンド1発でデプロイできる。 ただし、1回目と2回目以降でフローが異なる。 1回目ではsamconfig.tomlという設定ファイルを作成する。 2回目以降、作成済みのsamconfig.tomlを使ってデプロイが行われる。 $ sam deploy -g Configuring SAM deploy ====================== Looking for config file [samconfig.toml] : Not found Setting default arguments for \'sam deploy\' ========================================= Stack Name [sam-app]: AWS Region [ap-northeast-1]: #Shows you resources changes to be deployed and require a \'Y\' to initiate deploy Confirm changes before deploy [y/N]: y #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: y HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y Save arguments to configuration file [Y/n]: y SAM configuration file [samconfig.toml]: SAM configuration environment [default]: Looking for resources needed for deployment: Creating the required resources... Successfully created! Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-h0aw0pxx8pxv A different default S3 bucket can be set in samconfig.toml Saved arguments to config file Running \'sam deploy\' for future deployments will use the parameters saved above. The above parameters can be changed by modifying samconfig.toml Learn more about samconfig.toml syntax at https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html ... (省略) 最後の文節にあるように、samconfig.tomlを変更することで構成を変更できる。 この後、実際にCloudFormationスタックのアップロード/実行が走りリソースが組み上がる。 2回目以降、-gオプション抜きでsam deployを実行すると以下。 $ sam deploy File with same data already exists at sam-app/e32dcdf231268fbcad9915436e787001, skipping upload Deploying with following values =============================== Stack name : sam-app Region : ap-northeast-1 Confirm changeset : True Disable rollback : True Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-h0aw0pxx8pxv Capabilities : [\"CAPABILITY_IAM\"] Parameter overrides : {} Signing Profiles : {} Initiating deployment ===================== File with same data already exists at sam-app/9a813032a850e5b7fb214dffc5ac5783.template, skipping upload Waiting for changeset to be created.. Error: No changes to deploy. Stack sam-app is up to date Webコンソールで動作確認 Webコンソール上、生成されたLambda関数を確認できる。 HelloWorldが書かれたapp.jsが見える。 Testでサンプルイベントを送るとHelloWorldが200で返ってきた。OK。

default eye-catch image.

ACID特性 (ACID Property)

経験的にトランザクションの性質を知っている気になっているけれど、 ではACID特性のそれぞれを言葉で説明してみて, と言われると難しい. おそらくAtomicityだけをACID特性と言ってきた気がする. Wikipediaから. トランザクション分離レベルもこの際まとめておく. [arst_toc tag=\"h4\"] 不可分性(Atomicity) トランザクションに含まれるタスクが複数ある場合、全てのタスクが完全に完了するか、または全く実行されないか、いずれかであることを保証すること。 口座Aから口座Bに対して1万円送金する. 口座Aから1万円を引くタスクと口座Bに1万円を足すタスクの片方だけが実行されるとおかしなことになる. 両方のタスクが成功して取引引きが完了するか、両方のタスクが失敗して取引が失敗するかいずれか 一貫性 (Consistency) トランザクションの開始から終了までの間、操作対象のデータが正常範囲内に収まることを保証すること. 口座Aから口座Bに送金するケースで、口座Aに1万円しかないのに2万円送金しようとして一時的に口座Aが-1万円になることは一貫性に反する. 一貫性に反するイベントが発生したときにトランザクションを終了する. 独立性 (Isolation) トランザクション内の複数の操作は外部からは隠蔽されることを表す. 外部からはトランザクションの入りと出だけを知ることができる. 口座Aから口座Bに送金するケースで、口座Aから口座Bに1万円を送金する際に、中間状態として口座Aから1万円を減らしただけの状態が発生するものとする. 外部からは中間状態は見ることができず、口座Aから1万円が減り口座Bに1万円が足された状態のみを知り得る. 永続性 (Durability) DBMSの管理上の話. トランザクションが完了した場合,障害を受けたとしても完了後の状態を保持できることを表す. 通常、トランザクション操作はトランザクションログとしてストレージに記録される. トランザクションログはトランザクションの履歴で巻き戻したりできる. システムに異常が発生した場合、トランザクションんログを使って異常発生前の状態まで復旧できる. ACIDの現実 ACID特性を厳密に実装しようとすると、より広範囲のデータにアクセスする必要が発生する. 広範囲のデータにロックを掛けたり更新したりなどでパフォーマンスが落ちる. 実際はある程度妥協して実装される. ACID特性を実現する処理自体が失敗する可能性もある. ファイルシステムやバックアップ方式の工夫により冗長化する. 全ての処理を一度に実行することが求められるが、それは現実的には難しい. ログ先行書き込みとシャドウページング. トランザクション分離レベルを設定することで、トランザクションの並列実行時の厳密性とパフォーマンスのトレードオフを制御できる トランザクション分離レベル Dirty read. トランザクションAとトランザクションBが並列実行. AはBの途中の状態を見ることができる. Non-repeatable read (Fuzzy read). トランザクションAとトランザクションBが並列実行. Aが同じデータを2度読む. 1度目はBが書いていない. 2度目はBが書いている. Aから見て1度目と2度目のデータが異なるか消えているように見える. Phantom read. Non-repeatable readと似ているが、特にAの繰り返し読み込みの間にBがデータを挿入し、Aから見て突然新しいデータが出現したように見えること. 微妙な違いだが、過去から現在に渡って存在しているものの過去の状態が見えることと、過去存在していないが現在見えることは異なり、それぞれ名前がついている. ACID特性の厳密な実装にはパフォーマンス劣化とのトレードオフがあるため、 概念的に、使う側がトレードオフをコントロールできるようになっている. それがトランザクション分離レベル. あくまで概念のためDBMSによってその扱いが異なる. 分離レベル Dirty read Non-repeatable read Phantom read Read Uncomitted発生する発生する発生する Read Comitted発生しない発生する発生する Repeatable Comitted発生しない発生しない発生する Serializable発生しない発生しない発生しない

default eye-catch image.

Auroraの機能など

知識がないのに経験だけ積んだって力にならないんだよね。という話を聞いて腑に落ちた。 資格を取るために学んだことは、日々悩み考える色々な出来事を説明するための武器になる。 今自分は何をやろうとしていているのか、経験して後から回想するのでは余りに効率が悪い。 今回はAurora。やはり高いので個人では手が出ないのだけれど、 それなりの仕事であれば第1選択になり得る。 RDSと比較して圧倒的に高機能で運用時に困りそうなユースケースが通常の機能として既に備わっている. 参考書を1周したので、(著作権侵害にならないように)要約して自分の言葉でまとめていく。 [arst_toc tag=\"h4\"] クォーラムモデル コンピューティングリソースとストレージが分離している. コンピューティングとストレージを独立して管理する. コンピューティングリソース、ストレージ共に3AZに分散してレプリケートする. 1AZにコンピューティングリソース1台、ストレージ2台. 6台のストレージのうち2台が故障しても読み書き可. 3台が故障すると書き込みが不可となるが読み込み可. RDSはスタンバイレプリカとリードレプリカが別扱いだが Auroraはスタンバイ、リード共に共通. プライマリ、レプリカ、ストレージ(ボリューム)をセットでクラスタと呼ぶ. 可用性 読み書き可能なクラスターエンドポイント. 読み取り専用エンドポイント、任意のインスタンスにつなぐエンドポイントなどなど. クラスタ内の1台が読み書き用, 他は読み取り専用なので、読み書き用が落ちたときに読み取り専用が読み書き用に昇格する. これがフェイルオーバーの概念. クラスターエンドポイントに繋いでおくと、エンドポイント先で障害時に勝手にフェイルオーバーが発生する レプリカにはフェイルオーバー優先度をつけられる. 優先度が高い方が優先的にフェイルオーバー先になる. 同じだとインスタンスの大小で決まる. 多くの場合、フェイルオーバーの時間はRDSよりも短い. 通常、プライマリにのみキャッシュが効く. フェイルオーバーでキャッシュヒットしなくなる. クラスターキャッシュ管理をONにするとフェイルオーバー時に引き継がれる. 複数のリージョンに跨ってクォーラムモデルを配置するAuroraグローバルデータベース. DR対策. リージョン間のデータコピーは1秒未満. 複数のリージョンに跨ってクラスタを配置するクロスリージョンレプリケーション. DR対策. レプリカ間のデータコピーに時間がかかる. 通常クラスタ内の1台が読み書き可能で他は読み取り専用だが、全てを読み書き可能にできる. パフォーマンス 書き込み性能を上げるにはインスタンスサイズを上げる. Auroraレプリカはスタンバイレプリカ、リードレプリカを兼ねる. リードレプリカとして使うと読み込み性能が上がる. 読み込みエンドポイントは全ての読み込み用レプリカを代表する. アプリ側からは1個だが中は数台. Aurora AutoScaling. 読み込みクラスタのCPUまたは接続数が閾値以下になったときに自動スケールする. Aurora Serverless. インスタンス数,インスタンスサイズを自動スケールする. 未使用時に勝手に落ち,高負荷時に勝手に上がる. Aurora Serverlessは, 前提として利用頻度が少なくほとんど未使用だが、変化するときは大きく変化する、というアプリに適している. スケールアップは限界がある. つまり重量級のクエリの高速化には限界がある. スケールアウトはより柔軟なので多数のクエリの同時実行はより簡単に対応できる. セキュリティ 基本的にRDSと同様. VPC内に設置する. NACL、SGを使ってアクセス制御する. データ格納時・転送時に暗号化する. IAMロールを使ったクレデンシャルレス化. Auroraの監査機能は Advanced Auditing. 記録するクエリ種別を選択できる. CloudWatchLogsに転送可. コスト Auroraレプリカ1台ごとの稼働時間で課金. Aurora Serverlessはキャパシティユニット単位で課金. (cf.DynamoDB) RDSはインスタンスとストレージが密結合しているためストレージ容量はインスタンスに紐づく. インスタンス作成時に確保した量に課金. Auroraはストレージが分離しているためAuroraレプリカとは関係なく使った容量だけ重量課金. データを削除して未使用領域が出ると自動的に課金対象が減る. 通信はVPC外へのアウトバウンドにのみ課金. メンテナンス メンテナンスウインドウまたは即時で実行. Auroraではクラスター単位でパラメータを管理する.設定はクラスタ内のレプリカ全てに適用される. インスタンス単位のパラメータ管理もできる. ZDP(Zero Day Patch). ベストエフォートでダウンタイムなしのパッチ適用をおこなう. パッチ適用中も接続が維持される. バックアップ システムバックアップはメンテナンスウインドウで日時で行われる. データバックアップは継続的かつ増分的な自動バックアップ. 保持期間中の任意の時点へ復元できる. (PITR) データバックアップの保持期間は1日-35日. 0日(無効)には出来ない. S3に保存される. 手動でスナップショットを取得可能. システムバックアップ、データアップバック共に復元先は新しいAuroraクラスター. 保持期間の任意の時点を指定する. Auroraクローン. ストレージではなくコンピューティング部分のみをコピーする. リードレプリカ複製による読み取り性能向上. 1回でも書き込みしようとするとストレージ部分がコピーされる. データエクスポート、分析等読み込み専用のタスクに使う. Aurora MySQLでのみバックトラックを使用可能. 最大24時間前までSQL操作を遡れる. S3エクスポート. RDSはスナップショット作成操作だが, AuroraはSQLクエリ操作. モニタリング CloudWatchによるメトリクス監視. 拡張モニタリングによる詳細メトリクス監視. コンピューティングに関わる(CPU,メモリ等)をインスタンス単位のメトリクスとしてCloudWatchLogsで監視. ストレージに関わるクラスタ単位のメトリクスとしてCloudWatchLogsで監視. 監視が上手くいっているかを確認するため、障害を自力でシミュレートできる. 障害挿入クエリ. DBインスタンス,Auroraクラスタ,ディスクの障害. CloudWatchLogsよりもリアルタイム性があるデータベースアクティティストリーム. Amazon Kinesisにリアルタイムで入る. Kinesisに入ったデータストリームをElasticSearch等で可視化する. その他 Aurora MySQL. SQLからLambda関数を呼べる. Aurora MySQL. SQLからSageMakerエンドポイントを呼べる.

default eye-catch image.

RDSの機能など

参考書を1周した. 普段RDSを道具として使っているだけでは経験しない知識を得ることができた. インフラ系の仕事をしないと使わない可能性がある知識もあるが、アプリケーションエンジニアとしては、 RDSがここまでやってくれると知っていることで無駄な機能を作り込んだり、余計な心配をしなくて済む. [arst_toc tag=\"h3\"] 可用性 スケールアウトすることで何が冗長化されるのか. いざフェイルオーバーが発生したときどういう挙動になるのか. まとめ プライマリインスタンスとスタンバイレプリカを別AZに配置することで可用性を得る プライマリとスタンバイの間で常にデータ同期がおこなわれる プライマリに障害が発生した場合スタンバイにフェイルオーバーすることでDB接続を継続する スタンバイはトラフィック処理しない. 読み取り性能を上げるためにはリードレプリカを追加する スタンバイがある場合、スタンバイを対象にRDSのスナップショット取得がおこなわれ、プライマリのトラフィックに影響を与えない マルチAZの場合、スタンバイとのデータ同期によりシングル構成よりも書き込み・コミットでわずかにレイテンシが上がる AZの変更 プライマリのスナップショットを作成後、セカンダリとして復元し同期 AZ変更時はプライマリのパフォーマンスに影響する フェイルオーバー RDSの外からはエンドポイントでつなぐ プライマリに障害が発生した場合、エンドポイントの先が自動的にスタンバイにつなぎ変わる 切り替えにかかる時間は60秒-120秒. DNSキャッシュのTTLを60秒以内にしておくことが推奨されている AWSコンソールから手動でフェイルオーバー時の挙動を確認できる パフォーマンス スケールアップで何が良くなるのか。スケールアウトではどうか。 スケールアップさせることを前提にできるのか。 まとめ データベースのパフォーマンスは主にデータの読み書きのパフォーマンス 汎用SSD.3IOPS/GB. バースト(一時的に)100-10,000IOPS. プロビジョンドIOPS. 常に1,000-30,000IOPS. ストレージ容量の残量が10%以下の状態が5分以上続いた場合、5GBまたは割り当て容量の12%のどちらか大きい方が自動的に追加される 容量を頻繁に拡張できるわけではない.1度変更すると6時間変更できない. Storage Auto Scalingに頼るべきではない リードレプリカ 読み込み性能は、プライマリを複製したリードレプリカを増やすことで対応. トラフィックがリードレプリカに分散される 書き込み性能は、スケールアップにより対応 プライマリとリードレプリカの同期は非同期. 微妙に異なる. プライマリのスナップショットからリードレプリカが作成され複製される.従って作成直後は異なる リードレプリカは最大5個 プライマリとリードレプリカのインスタンスサイズは異なっていても良い 手動でリードレプリカをプライマリに昇格可能 マルチAZ可能. DR対応で別リージョンにリードレプリカを作成可能. リードレプリカのエンドポイントはそれぞれ異なる.負荷分散する場合、Route53等で1つのDNSレコード先を分散させる RDBMSごとの制約 SQLServerの場合、特定エディション以上でリードレプリカを使用可能 SQLServerの場合、マルチリージョン、マルチAZリードレプリカを作成不可 Oracleの場合、特定エディション以上でリードレプリカを使用可能 Oracleの場合、OracleのActiveDataGuargeにより同期がおこなわれる RDS Proxy アプリケーションがDBにアクセスする際、一度作成したコネクションをプーリングして使い回す機能 昔、LambdaからRDSにつなぐ際、コネクションがプールされずすぐに最大接続数を超過していたがこれで解決した RDS Proxyはプライマリインスタンスのみ対応 セキュリティ アプリケーションが個人情報の暗号化を意識する必要があるのか。RDSが透過的に面倒を見てくれるのか。 まとめ RDSを設置するVPCには少なくとも2つのサブネットが必要 VPCのACL、SGでアクセス制御する SGの送信元にはSGを指定できる.SGとSGの接続を定義できる 暗号化 データ格納時の暗号化と通信時の暗号化の2つ KMSのキーを使用して格納するデータを暗号化. KMSキーを別管理することでRDS内のデータが漏れても保護できる KMS暗号化は透過的におこなわれる. アプリケーションは特に意識しなくても良い 暗号化の対象は以下の通り DBインスタンスに格納するデータ 自動バックアップ リードレプリカ スナップショット ログファイル DBインスタンス作成時にのみ暗号化可能. 未暗号化インスタンスのスナップショットを作成して復元時に暗号化 プライマリだけ、リードレプリカだけ、のように非対称に暗号化することはできない KMSはリージョンを跨げないためリージョン間スナップショットを取る場合はコピー先のリージョンでコピー元とは異なるKMSキーを指定する必要がある SSL/TLSにより伝送中データの暗号化 AWSからルート証明書をDLしアプリケーション側でSSL/TLS通信時に取得したルート証明書を使う ルート証明書は定期的に失効する. 都度ダウンロードして更新すること IAMによるDBアクセス認証 MySQLとPostgreSQLに限り、IAMを使用したDBアクセス認証を利用できる. RDSへアクセス可能なIAMロールを作成. アプリケーション側は作成されたIAMロールを使ってRDSにアクセス アプリケーション側で接続情報を管理しなくてもよい 監査ログ DBエンジンがもつ監査ログ機能を利用できる. 監査ログはCloudWatchに転送され、管理・監視できる コスト アプリケーション側をチューニングする人的コストと、インスタンスに使うコスト。 何に料金がかかるということを把握して、アプリケーション側でやるべきこと/AWS側に振ることを意識する. まとめ RDSで発生するコストはインスタンス料金、ストレージ料金、データ通信料金 インスタンス料金 コストは1秒単位.ただし1時間未満は最低10分から. 2AZに配置した場合、リードレプリカを設置した場合、インスタンス数が2倍になるのでインスタンス料金も2倍になる DBエンジンの種類によって若干インスタンス料金が異なる.MySQL<postgreSQL<oracle 1年または3年の前払い制(リザーブドインスタンス)により割安になる.損益分岐点あり インスタンスを停止するとインスタンス料金の課金は止まる.ただし1週間止めておくと自動的に起動してしまう. ストレージ料金 インスタンスを止めていてもストレージ料金の課金は止まらない 利用中のストレージサイズと同サイズまでのバックアップには課金されない.それを超えたところから課金される.ただし超えた分は安い データ転送料金 RDSへのINは無料 RDSからVPC外部、またはインターネットへの通信は課金される. 通常VPC内部でEC2とやりとりする場合は無料だが、VPC外部とやりとりする場合注意 メンテナンス 作ったアプリが保守フェーズに移行した後、アプリケーション側は何を意識しなければならないか. まとめ AWSが実施するメンテナンスの実行時間を指定できる.(メンテナンスウインドウ) 22:00-06:00の間の30分. 大きなメンテナンスの場合1時間かかる場合がある.余裕をみて1時間設定する メンテナンスウインドウ期間中、いくつかのメンテナンスによりインスタンスが一時的にオフラインになる メンテナンス種別は「必須」と「利用可能」. 「必須」は無期限延期できない. 「利用可能」はできる. アプリケーションの動作に影響がありそうなものは開発環境で事前に検証すること マルチAZのメンテナンス まずスタンバイについてメンテナンスを実行 スタンバイをプライマリに昇格. 降格した元プライマリにメンテナンスを実行.そのままスタンバイになる 全体としてインスタンスがオフラインになることがない. ストレージ追加、インスタンスタイプの変更は任意またはメンテナンスウインドウ DBエンジンのアップグレード メジャーバージョンアップはユーザ自身が実施 マイナーバージョンアップは設定次第で自動でやってくれる. 手動でも可. パラメータグループ 設定値(パラメータ)のグループ. 例えばMySQLのconfに書くような設定値が集まったもの. DBエンジンごとに様々なパラメータが存在する デフォルトパラメータグループ ユーザは変更できない. ユーザが独自のパラメータグループを作成しデフォルトパラメータをオーバーライド すぐに適用される「動的パラメータグループ」.再起動が必要な「静的パラメータグループ」 追加設定はオプショングループ.デフォルトのパラメータは変更できず,ユーザが作成してオーバーライド バックアップ これも, 保守フェーズに移行した後アプリケーション側で何を意識しないといけないか. 自動バックアップと手動バックアップ 自動バックアップ 自動的にスナップショットを保存. 保存日数はデフォルト7日.0(無効)-35日. スナップショットは不可視のS3に保存される. 初回のスナップショットはフル. 2回目以降は差分. バックアップはメンテナンスウインドウで作成される. シングルAZの場合一時的にオフラインになる. マルチAZの場合オフラインにならない 手動バックアップ 任意のタイミングでバックアップできる. 手動バックアップは自動的に削除されない. DR目的で別リージョンへのスナップショットコピー 別リージョンに手動でスナップショットをコピーできる 暗号化用KMSキー、オプショングループは自動でコピーされないので自力でコピー先に作る 別アカウントとスナップショット共有 手動バックアップしたスナップショットを別アカウントと共有できる 暗号化済みの場合、KMSキーを共有先にアクセス許可する 暗号化していない場合、格納された個人情報にアクセス可能となる スナップショットの復元 既存のRDSインスタンスに復元できない.新しいRDSインスタンスを復元する エンドポイントが変わるのでアプリケーション側の再設定が必要 パラメータグループはインスタンスに紐づくため復元時に復元元のパラメータグループを使用する PITR(ポイントインタイムリカバリ) スナップショットとは別にトランザクションログがS3に5分単位で保存される スナップショット復元と合わせて最短で5分前までの状態に復元が可能. S3へのエクスポート スナップショットからS3にエクスポートできる 不可視のS3ではなく、Amazon Parquet形式でS3バケットにデータをエクスポートできる Athena、Redshift等別サービスからS3上のファイルを検索、分析できる モニタリング 作ったアプリがショボすぎて速度が出ない! ピンチを救うAWSの機能. 保守フェーズ移行, 劣化やユーザ数増加により受けた影響の調査. 他. インスタンスが効率的に使われているかを調べるためにリソース使用状況を監視できる CloudWatchにメトリクスが展開される. CloudWatchAlarmによりメトリクスの変化に伴ってSNS通知などアクションを実行できる DBエンジンが出力するログはCloudWatchLogsに転送できる ログに含まれる特定のエラー文字列を見つけてSNS通知するなどのユースケース 拡張モニタリングにより詳細なリソースデータを監視できる. パフォーマンスインサイト. パフォーマンスに関するデータを可視化する. ユーザ自身が可視化ツールを用意しなくてもある程度は確認できる スロークエリ、実行計画の確認などができる. パフォーマンスチューニングの初手に使える フェイルオーバーや再起動などをトリガーとしてSNS通知できる

default eye-catch image.

PostgreSQL ダブルクォートで括られたDB名は大文字と小文字が区別される. FATAL: database “” does not exist .

pg_dumpコマンドを使って作られたdumpファイルをpg_restoreでリストアしようとしたところ、 DB名が無いと怒られた. dbnameという名前のDBは確かにあるはずなのに. psql: FATAL: database \"DBNAME\" does not exist 理由は、dumpファイル内で、ダブルクォートで括られた\"DB名\"に対して操作をしようとしていたから. \"DBNAME\"に対する操作は全て大文字の\"DBNAME\"に対する操作を表していて、 \"dbname\"と\"DBNAME\"は区別される。 CREATE DATABASE \"DBNAME\" OWNER = postgres TEMPLATE = template0 ENCODING = \'UTF8\' LC_COLLATE = \'C\' LC_CTYPE = \'C\'; 識別子を小文字で統一できるならそうした方が良い. 既に何かがあり出来ない状況で出くわしたなら、大文字と小文字を意識する必要がある. 以下PostgreSQLの公式ドキュメント. PostgreSQLにおいて、識別子をダブルクォートで括ると、大文字小文字が区別されるようになる. 例えば、以下のようにCREATE DATABASEを叩くと、全て大文字の DBNAME というDBが作られる. PostgreSQL 12.4文書/ 4.1.1. 識別子とキーワード

default eye-catch image.

Terraformを使ってAWSにWebアプリケーションの実行環境を立てる (EC2立てるまで)

Webアプリケーション実行環境をIaCで管理したい. Terraformでクラウド構成を作ってAnsibleでミドルウェアをインストールしたい. BeanstalkやLightsailのようなPaaSではなくTerraformを使ってVPCから自前で作ってみる. この記事はEC2を立てるまでが範囲. 次の記事でAnsibleを使って立てたEC2にミドルウェアをインストールする. [arst_toc tag=\"h4\"] この記事で紹介する範囲 この記事ではTerraformを使ってAWS上に以下の構成を作るまでを書いてみる. とはいえTerraformの習得が8割くらいのモチベなので実用性はあまり重視しない. サブネットをプライベートとパブリックに分けてみたい. プライベートにDB(MySQL), パブリックにWebサーバ(nginx). ひとまずALBは配置しない. Terraformの導入 Ansibleもそうだけれども, アプリを保守している期間って割と長いもので、 その間, 構成管理ツール側のバージョンが上がってしまう傾向がある. そうすぐに古い書き方が使えなくなることはないが, 警告が出まくって気分がよくない. 構成管理ツールの古いバージョンを残しておきたい, どのバージョンを使うか選びたい, という期待がある. rbenvやpyenvのようにTerraform自体のバージョンを管理するtfenvをインストールしておき, この記事を書いた日の最新である 1.0.3 をインストールすることにする. $ brew install tfenv $ tfenv --version tfenv 2.2.2 $ tfenv list-remote .1.0-alpha20210714 1.1.0-alpha20210630 1.1.0-alpha20210616 1.0.3 1.0.2 1 ... $ tfenv install 1.0.3 ... $ tfenv list 1.0.3 $ tfenv use 1.0.3 Switching default version to v1.0.3 Switching completed $ terraform version Terraform v1.0.3 on darwin_amd64 git secretsの導入 AWSのcredentialsなどを誤ってcommitしてしまう事故を防ぐためにgit secretsを導入する. commit時に内容を検証してくれて, もしそれらしきファイルがあればリジェクトしてくれる. どこまで見てくれるのか未検証だけれども入れておく. Laravelの.env_staging等に書いたcredentialsがどう扱われるか後で検証する. $ brew install git-secrets $ git secrets --install ✓ Installed commit-msg hook to .git/hooks/commit-msg ✓ Installed pre-commit hook to .git/hooks/pre-commit ✓ Installed prepare-commit-msg hook to .git/hooks/prepare-commit-msg $ git secrets --register-aws OK ディレクトリ構成 勉強用の小さな環境を作るのだけれども, 今後の拡張性については考慮しておきたい. 割と規定されている傾向があるAnsibleと比較して,Terraformは自由な印象. 以下の記事を参考にさせて頂きました. Terraformなにもわからないけどディレクトリ構成の実例を晒して人類に貢献したい iac ├── dev │ ├── backend.tf │ ├── main.tf -> ../shared/main.tf │ ├── provider.tf -> ../shared/provider.tf │ ├── versions.tf -> ../shared/versions.tf │ ├── terraform.tfvars │ └── variables.tf -> ../shared/variables.tf └── shared ├── main.tf ├── provider.tf ├── variables.tf └── modules ├── vpc │ ├── eip.tf │ ├── internet_gateway.tf │ ├── nat_gateway.tf │ ├── routetables.tf │ ├── subnet.tf │ ├── vpc.tf │ ├── outputs.tf │ └── variables.tf └── ec2 ├── ec2.tf ├── keypair.tf ├── network_interface.tf ├── security_group.tf ├── outputs.tf └── variables.tf tfstateの保存先の定義 tfstate は Terraformが管理しているリソースの現在の状態を表すファイル. terraformは「リソースを記述したファイル」と「現在の状態」の差分を埋めるように処理を行うが, いちいち「現在の状態」を調べにいくとパフォーマンスが悪化するため, ファイルに保存される. (確かにAnsibleは毎回「現在の状態」を調べにいっているっぽく,これが結構遅くて毎回イライラする) デフォルトだとローカルに作られるが, それだとチーム開発で共有できないので, S3等に作るのが良くあるパターン. Terraformでは\"バックエンド\"という概念で扱われる. \"バックエンド\"を以下のように記述する. バックエンドの定義はterraformの前段にあり, S3 bucketとDynamoDB tableを手動で作っておく必要がある. 変数を使うことができないのでハードコードしないといけない. 議論があるらしい. key,secretを書く代わりにprofileを書くことで, 構成管理可能になる. (同じprofile名をチームで共有しないといけない...) backendをS3にする際にS3のbucketをどう作るか問題はいろいろ議論があるようで, いずれ以下の記事を参考にしてよしなにbucketを作れるようにしたい. dynamodb_tableを設定すると、そこにロックファイルを作ってくれるようになる. 多人数で同じ構成管理を触るときに便利. Backend の S3 や DynamoDB 自体を terraform で管理するセットアップ方法 terraform { backend \"s3\" { region = \"ap-northeast-1\" profile = \"ikuty\" bucket = \"terraform-state-dev\" key = \"terraform-state-dev.tfstate\" dynamodb_table = \"terraform-state-lock-dev\" } } credentialsの書き方 ルートにある terraform.tfvarsというファイルを置いておくと、 そこに記述した内容を変数に注入することができる. \"注入\"という言葉で良いのか不明だが、定義した変数の初期値を設定してくれる. credentialsを構成管理に登録するのはご法度. terraform.tfvarsを構成管理外として何らかの方法で環境にコピーする. 多くのツールで採用されている「よくあるパターン」. 他に,applyコマンドに直接渡したり, 環境変数で指定したりできるが, Terraform公式は.tfvarsを推奨している. aws_access_key_id = \"AKI*****************\" aws_secret_access_key = \"9wc*************************************\" aws_region = \"ap-northeast-1\" providerの定義 プロバイダとは, 要は\"AWS\",\"Azure\",\"GCP\".. のような粒度の何か. Terraformは結構な種類のプロバイダに対応していて「どのプロバイダを使うか」を定義する. 今回はAWSを使う. dev.tfvarsに記述しておいたCredentialsを変数で受けて設定する. 以下,変数の定義方法, デフォルト値の設定方法を示している. .tfvarsに記述した同名の変数について,terraformが値を設定してくれる. variable \"aws_access_key_id\" {} variable \"aws_secret_access_key\" {} variable \"aws_region\" { default = \"ap-northeast-1\" } provider \"aws\" { access_key = \"${var.aws_access_key_id}\" secret_key = \"${var.aws_secret_access_key}\" region = \"${var.aws_region}\" } エントリポイント Terraformのエントリポイントはルートに置いた\"main.tf\". ディレクトリ構成を凝らないのであれば、main.tf に全てをベタ書きすることもできる. 今回、devやstg, prod のような環境ごとにルートを分ける構成を作りたいのだが、 main.tf 自体は環境ごとに差異が無いことを前提にしている. ./shared/main.tf というファイルを作成し、 各環境ごとの main.tf を ./shared/main.tf の Symbolic Link とする. main.tf でリソースの定義はおこなわない. 同階層の./modules にモジュール定義があるが, main.tf は ./modules以下の各モジュールに変数を渡すだけ. VPCの作成とEC2の作成を各モジュールに分割した. 各モジュールのOutputのスコープはモジュールまでなので、 例えばEC2モジュールからVPCモジュールのVPC IDを直接受け取れない. main.tf はモジュールの上に位置するため、このようにモジュール間で変数を共有できる. module \"vpc\" { source = \"../shared/modules/vpc\" } module \"ec2\" { source = \"../shared/modules/ec2\" vpc_id = module.vpc.myVPC.id private_subnet_id = module.vpc.private_subnet.id public_subnet_id = module.vpc.public_subnet.id } VPCモジュール ./shared/modules/vpc以下にVPCモジュールを構成するファイルを配置する. スコープがVPCモジュールに閉じたローカル変数を定義する. 以下のようにしておくと、モジュール内から local.vpc_cidr.dev のように値を取得できる. locals { vpc_cidr = { dev = \"10.1.0.0/16\" } subnet_cidr = { private = \"10.1.2.0/24\" public = \"10.1.1.0/24\" } } VPCを1個作る. VPCのCIDRは10.1.0.0/16. resource \"aws_vpc\" \"myVPC\" { cidr_block = local.vpc_cidr.dev instance_tenancy = \"default\" enable_dns_support = \"true\" enable_dns_hostnames = \"false\" tags = { Name = \"myVPC\" } } 作ったVPC内にサブネットを2個作る. 1つはPrivate用. もう1つはPublic用. PrivateサブネットのCIDRは10.1.2.0/24. PublicサブネットのCIDRは10.1.1.0/24. AZは両方同じで \"ap-northeast-1a\". map_public_ip_on_launchをtrueとしておくと, そこで立ち上げたEC2に自動的にpublic ipが振られる. resource \"aws_subnet\" \"public_1a\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_vpc.myVPC] availability_zone = \"ap-northeast-1a\" cidr_block = local.subnet_cidr.public map_public_ip_on_launch = true tags = { Name = \"public-1a\" } } resource \"aws_subnet\" \"private_1a\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_vpc.myVPC] availability_zone = \"ap-northeast-1a\" cidr_block = local.subnet_cidr.private tags = { Name = \"private-1a\" } } VPCに紐づくInternet Gatewayを作る. resource \"aws_internet_gateway\" \"myGW\" { vpc_id = \"${aws_vpc.myVPC.id}\" depends_on = [aws_vpc.myVPC] tags = { Name = \"my Internet Gateway\" } } Privateサブネットからインターネットに繋ぐために、 PublicサブネットにNAT Gatewayを作りたい. NAT Gateway用のEIPを作る. resource \"aws_eip\" \"nat_gateway\" { vpc = true depends_on = [aws_internet_gateway.myGW] tags = { Name = \"Eip for Nat gateway\" } } PublicサブネットにNAT Gatewayを作る. EIPは上で作成したものを使う. resource \"aws_nat_gateway\" \"myNatGW\" { allocation_id = aws_eip.nat_gateway.id subnet_id = aws_subnet.public_1a.id depends_on = [aws_internet_gateway.myGW] tags = { Name = \"my Nat Gateway\" } } ルートテーブル. いろいろなところで書かれていた内容を試してようやく動くものができた. VPCにはデフォルトで「メインルートテーブル」が作られる. メインルートテーブルはいじっていない. 以下、Private, Publicサブネットそれぞれのためのルートテーブルを定義している. PublicサブネットからInternet Gatewayに繋ぐ. PrivateサブネットからNAT Gatewayに繋ぐ. # Route table for public # public resource \"aws_route_table\" \"public\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_internet_gateway.myGW] tags = { Name = \"my Route Table for public\" } } # private resource \"aws_route_table\" \"private\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_internet_gateway.myGW] tags = { Name = \"my Route Table for private\" } } # Route table association # public resource \"aws_route_table_association\" \"public\" { subnet_id = aws_subnet.public_1a.id route_table_id = aws_route_table.public.id } # private resource \"aws_route_table_association\" \"private\" { subnet_id = aws_subnet.private_1a.id route_table_id = aws_route_table.private.id } # Routing for public resource \"aws_route\" \"public\" { route_table_id = aws_route_table.public.id gateway_id = aws_internet_gateway.myGW.id destination_cidr_block = \"0.0.0.0/0\" } # Routing for private resource \"aws_route\" \"private\" { route_table_id = aws_route_table.private.id gateway_id = aws_nat_gateway.myNatGW.id destination_cidr_block = \"0.0.0.0/0\" } EC2モジュール ./shared/modules/ec2以下にEC2モジュールを構成するファイルを配置する. スコープがEC2モジュールに閉じたローカル変数を定義する. main.tfからVPCモジュールのOutputをEC2モジュールに渡す必要があるが、 渡すデータを受けるためにEC2モジュール側で変数を定義しておく必要がある. locals { private = { ip = \"10.1.2.5\" ami = \"ami-0df99b3a8349462c6\" instance_type = \"t2.micro\" } public = { ip = \"10.1.1.5\" ami = \"ami-0df99b3a8349462c6\" instance_type = \"t2.micro\" } } variable \"vpc_id\" { type = string } variable \"private_subnet_id\" { type = string } variable \"public_subnet_id\" { type = string } EC2にアクセスするための鍵ペア. 既に鍵ペアを持っているものとし、その公開鍵を渡す. 以下のようにすると、HostからSSHの-iオプションで秘密鍵を指定して接続できるようになる. resource \"aws_key_pair\" \"deployer\" { key_name = \"deployer\" public_key = \"{公開鍵}\" } EC2に設定するセキュリティグループを作る. この記事では, Private, Publicともに、インバウンドをSSHのみとした. 次の記事でPublicにHTTPを通す. アウトバウンドとして全て通すようにしないとインスタンスから外にアクセスできなくなる(ハマった). # Security group resource \"aws_security_group\" \"web_server_sg\" { name = \"web_server\" description = \"Allow http and https traffic.\" vpc_id = var.vpc_id } # Security group rule SSH(22) resource \"aws_security_group_rule\" \"web_inbound_ssh\" { type = \"ingress\" from_port = 22 to_port = 22 protocol = \"tcp\" cidr_blocks = [\"0.0.0.0/0\"] security_group_id = aws_security_group.web_server_sg.id } resource \"aws_security_group_rule\" \"web_outbound\" { type = \"egress\" from_port = 0 to_port = 0 protocol = \"-1\" cidr_blocks = [\"0.0.0.0/0\"] ipv6_cidr_blocks = [\"::/0\"] security_group_id = aws_security_group.web_server_sg.id } # Security group resource \"aws_security_group\" \"db_server_sg\" { name = \"db_server\" description = \"Allow MySQL traffic.\" vpc_id = var.vpc_id } # Security group rule SSH(22) resource \"aws_security_group_rule\" \"db_inbound_ssh\" { type = \"ingress\" from_port = 22 to_port = 22 protocol = \"tcp\" cidr_blocks = [\"0.0.0.0/0\"] security_group_id = aws_security_group.db_server_sg.id } resource \"aws_security_group_rule\" \"db_outbound\" { type = \"egress\" from_port = 0 to_port = 0 protocol = \"-1\" cidr_blocks = [\"0.0.0.0/0\"] ipv6_cidr_blocks = [\"::/0\"] security_group_id = aws_security_group.db_server_sg.id } ネットワークインターフェース. セキュリティグループはEC2インスタンスではなくネットワークインターフェースに紐づく. EC2(aws_instance)のsecurity_groupsに書けなくてハマった. # public resource \"aws_network_interface\" \"public_1a\" { subnet_id = var.public_subnet_id private_ips = [local.public.ip] security_groups = [ aws_security_group.web_server_sg.id ] tags = { Name = \"public_subnet_network_interface\" } } # private resource \"aws_network_interface\" \"private_1a\" { subnet_id = var.private_subnet_id private_ips = [local.private.ip] security_groups = [ aws_security_group.db_server_sg.id ] tags = { Name = \"private_subnet_network_interface\" } } 最後にEC2. # Web Server resource \"aws_instance\" \"public\" { ami = local.public.ami instance_type = local.public.instance_type key_name = aws_key_pair.deployer.id network_interface { network_interface_id = aws_network_interface.public_1a.id device_index = 0 } credit_specification { cpu_credits = \"unlimited\" } root_block_device { volume_size = 20 volume_type = \"gp2\" delete_on_termination = true tags = { Name = \"web-ebs\" } } tags = { Name = \"Web\" } } # DB Server resource \"aws_instance\" \"private\" { ami = local.private.ami instance_type = local.private.instance_type key_name = aws_key_pair.deployer.id network_interface { network_interface_id = aws_network_interface.private_1a.id device_index = 0 } credit_specification { cpu_credits = \"unlimited\" } root_block_device { volume_size = 20 volume_type = \"gp2\" delete_on_termination = true tags = { Name = \"db-ebs\" } } tags = { Name = \"DB\" } } 実行 作った.tfファイルを再生して環境を構築する. validateでデバッグして、大体できたらplan(DryRun)で変更が正しそうか確認してみた. が、評価しなければわからないものについてはDryRunではわからず、 結局applyが途中で止まって解決しないといけない. ansibleと異なり冪等性が言われていなくて、applyで間違った構成を作ってしまうと、 その先、その構成を修正したとしても上手くいかないことがある. $ cd \"/path/to/dev\" $ terraform validate Success! The configuration is valid. $ terraform plan ... $ terraform apply ... 出来たとして、Publicに立ったEC2のパブリックIPv4をメモる. 疎通確認 Host ->(SSH)-> Web ->(SSH)-> DB を試す. DBから外に繋がるか試す. SSH Agent Forwardを使うと、Web EC2に秘密鍵を置かないで済む. Web側のssh configにForwardAgent yesを指定しておく. Host db HostName 10.1.2.5 User ubuntu ForwardAgent yes いざ. $ ssh-add \"{秘密鍵のパス}\" $ ssh -A ubuntu@{WebのパブリックIPv4} ubuntu@ip-10-1-1-5$ ssh db ubuntu@ip-10-1-2-5$ ping yahoo.co.jp 64 bytes from f1.top.vip.kks.yahoo.co.jp (183.79.135.206): icmp_seq=1 ttl=33 time=14.9 ms 64 bytes from f1.top.vip.kks.yahoo.co.jp (183.79.135.206): icmp_seq=2 ttl=33 time=14.6 ms .. できた..

default eye-catch image.

Grafanaプラグインを読んでいく – Clock plugin

最も単純そうなプラグインを読んでいくシリーズ。 プラグインは Clock plugin。Panelプラグイン。配布はここ。 最も単純そうなDataSourceプラグインを読む記事は以下。 [clink url=\"https://ikuty.com/2020/11/14/grafana-code-read/\"] ダッシュボードに時計を表示できる。 ダッシュボードから設定をおこない表示に反映する機能を備えていて、 PanelプラグインのHelloWorldには良い感じ。 The Clock Panel can show the current time or a countdown and updates every second. Show the time in another office or show a countdown to an important event. 肝心のデータプロットに関する機能は無いので別途違うコンポーネントを読む。 インストール、ビルド 公式からインストールすると src が含まれない。 ソースコードをclone、buildすることにする。 初回だけgrafana-serverのrestartが必要。 # clone repository $ cd ~/ $ git clone https://github.com/grafana/clock-panel.git $ mv clock-panek /var/lib/grafana/plugins # install plugin $ yarn install $ yarn build # restart grafana-server $ sudo service grafana-server restart ディレクトリ・ファイル構成 ディレクトリ・ファイル構成は以下の通り。 clock-panel/ src/ ClockPanel.tsx ... プラグイン本体 module.ts ... プラグインのエントリポイント options.tsx plugin.json ... プラグインの設定ファイル types.ts ... TypeScript型定義 img/ ... 画像リソース clock.svg countdown1.png screenshot-clock-options.png screenshot-clocks.png screenshot-showcase.png external/ ... 外部ライブラリ moment-duration-formant.js エントリポイント ./module.ts の内容は以下の通り。 ClockPanel.tsxで定義済みのClockPanelクラスをexportしている。 options.tsxに記述したオプション画面関連のクラスを.setPanelOptions()を介して設定する。 import { PanelPlugin } from \'@grafana/data\'; import { ClockPanel } from \'./ClockPanel\'; import { ClockOptions } from \'./types\'; import { optionsBuilder } from \'./options\'; export const plugin = new PanelPlugin(ClockPanel).setNoPadding().setPanelOptions(optionsBuilder); 本体 (ClockPanel.tsx) ./ClockPanel.tsxを読んでいく。React+TypeScript。 import React, { PureComponent } from \'react\'; import { PanelProps } from \'@grafana/data\'; import { ClockOptions, ClockType, ZoneFormat, ClockMode } from \'./types\'; import { css } from \'emotion\'; // eslint-disable-next-line import moment, { Moment } from \'moment\'; import \'./external/moment-duration-format\'; interface Props extends PanelProps {} interface State { // eslint-disable-next-line now: Moment; } export function getTimeZoneNames(): string[] { return (moment as any).tz.names(); } // PureComponentクラスを派生させることでプラグイン用のパネルクラスを定義できる。 // パネルのプロパティは PanelProps型だが 当プラグイン用にProps型に拡張している。 export class ClockPanel extends PureComponent { timerID?: any; state = { now: this.getTZ(), timezone: \'\' }; //Componentのインスタンスが生成されDOMに挿入されるときに呼ばれる //DOM挿入後,1秒間隔で this.tick()の実行を開始する。 componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 // 1 second ); } //[非推奨] DOMから削除されるときに呼ばれる。古いのかな。 //this.tick()の実行を停止する。 componentWillUnmount() { clearInterval(this.timerID); } //DOM挿入後1秒間隔で呼ばれる。 //stateを更新する。 tick() { const { timezone } = this.props.options; this.setState({ now: this.getTZ(timezone) }); } //時刻フォーマットを取得する。 //時刻フォーマットはオプション設定画面で設定され props.optionsに渡される。 //渡される変数は clockType と timeSettings である。 clockTypeは 12時間/24時間のいずれか。 //12時間なら h:mm:ss A, 24時間なら HH:mm:ss。 //カスタムの場合,clockTypeがClockType.Customになる。 getTimeFormat() { const { clockType, timeSettings } = this.props.options; if (clockType === ClockType.Custom && timeSettings.customFormat) { return timeSettings.customFormat; } if (clockType === ClockType.H12) { return \'h:mm:ss A\'; } return \'HH:mm:ss\'; } // Return a new moment instnce in the selected timezone // eslint-disable-next-line getTZ(tz?: string): Moment { if (!tz) { tz = (moment as any).tz.guess(); } return (moment() as any).tz(tz); } //カウントダウン文字列を得る //設定値countdownSettings, timezone は props.options から得られる。 // getCountdownText(): string { const { now } = this.state; const { countdownSettings, timezone } = this.props.options; //カウントダウン終了時 設定された文字列 countdownSettings.endText を返す if (!countdownSettings.endCountdownTime) { return countdownSettings.endText; } //残り時間を計算。 const timeLeft = moment.duration( moment(countdownSettings.endCountdownTime) .utcOffset(this.getTZ(timezone).format(\'Z\'), true) .diff(now) ); let formattedTimeLeft = \'\'; //計算した残り時間が0以下であれば、設定された文字列 countdownSettings.endText を返す。 if (timeLeft.asSeconds() 0) { formattedTimeLeft = timeLeft.years() === 1 ? \'1 year, \' : timeLeft.years() + \' years, \'; previous = \'years\'; } // Y months (or Y month) if (timeLeft.months() > 0 || previous === \'years\') { formattedTimeLeft += timeLeft.months() === 1 ? \'1 month, \' : timeLeft.months() + \' months, \'; previous = \'months\'; } // Z days (or Z day) if (timeLeft.days() > 0 || previous === \'months\') { formattedTimeLeft += timeLeft.days() === 1 ? \'1 day, \' : timeLeft.days() + \' days, \'; previous = \'days\'; } // A hours (or A hour) if (timeLeft.hours() > 0 || previous === \'days\') { formattedTimeLeft += timeLeft.hours() === 1 ? \'1 hour, \' : timeLeft.hours() + \' hours, \'; previous = \'hours\'; } // B minutes (or B minute) if (timeLeft.minutes() > 0 || previous === \'hours\') { formattedTimeLeft += timeLeft.minutes() === 1 ? \'1 minute, \' : timeLeft.minutes() + \' minutes, \'; } // C minutes (or C minute) formattedTimeLeft += timeLeft.seconds() === 1 ? \'1 second \' : timeLeft.seconds() + \' seconds\'; return formattedTimeLeft; } //Zoneを表示するh4タグを作成して返す。Reactっぽい。 renderZone() { const { now } = this.state; const { timezoneSettings } = this.props.options; const { zoneFormat } = timezoneSettings; // ReactでCSSを書く作法。ヒアドキュメント // ロジックコードの中にHTML生成コードが混在して非常に見辛い。 const clazz = css` font-size: ${timezoneSettings.fontSize}; font-weight: ${timezoneSettings.fontWeight}; line-height: 1.4; `; let zone = this.props.options.timezone || \'\'; switch (zoneFormat) { case ZoneFormat.offsetAbbv: zone = now.format(\'Z z\'); break; case ZoneFormat.offset: zone = now.format(\'Z\'); break; case ZoneFormat.abbv: zone = now.format(\'z\'); break; default: try { zone = (this.getTZ(zone) as any)._z.name; } catch (e) { console.log(\'Error getting timezone\', e); } } return ( {zone} {zoneFormat === ZoneFormat.nameOffset && ( ({now.format(\'Z z\')}) ); } //Dateを表示するh3タグを返す。 renderDate() { const { now } = this.state; const { dateSettings } = this.props.options; const clazz = css` font-size: ${dateSettings.fontSize}; font-weight: ${dateSettings.fontWeight}; `; const disp = now.locale(dateSettings.locale || \'\').format(dateSettings.dateFormat); return ( {disp} ); } //Timeを返すh2タグを返す。 renderTime() { const { now } = this.state; const { timeSettings, mode } = this.props.options; const clazz = css` font-size: ${timeSettings.fontSize}; font-weight: ${timeSettings.fontWeight}; `; const disp = mode === ClockMode.countdown ? this.getCountdownText() : now.format(this.getTimeFormat()); return {disp}; } //React componentとしてrender()メソッドを実装する必要がある。 //CSSを整形してZone,Date,Timeを設定して返す。 render() { const { options, width, height } = this.props; const { bgColor, dateSettings, timezoneSettings } = options; const clazz = css` display: flex; align-items: center; justify-content: center; flex-direction: column; background-color: ${bgColor ?? \'\'}; text-align: center; `; return ( {dateSettings.showDate && this.renderDate()} {this.renderTime()} {timezoneSettings.showTimezone && this.renderZone()} ); } } 設定 (options.tsx) 設定画面側を読んでいく。 PanelOptionsEditorBuilder型の引数を取り、builderに対して機能実装していく。 機能実装というのは、つまり、ラジオボタンを追加したり、カスタムエディタを追加したり、など。 この実装で以下のような設定画面が表示される。(README.mdは古いので注意)。 Modeとして、時間をそのまま表示するTimeモードか、カウンドダウンモードかを二者択一で設定する。 背景色(BackgroundColor)をカラーピッカーで設定する。(ちなみにGrafanaV7では機能しない様子)。 addTimeFormat()メソッドにより、24h表示/12h表示/カスタム表示,FontSize,FontWeightの設定機能を追加する。 addTimeZone()メソッドにより,TimeZoneと表示有無の設定機能を追加する。 カウントダウンモードに設定すると、カウントダウン設定をおこなえるが, addCountdown()メソッドにより,カウントダウン設定を追加する。 決められた構文にしたがって欲しい機能を追加していくだけなので、 設定画面の実装が必要になったら必要な構文を調べて追加していくことになりそう。 import React from \'react\'; import { PanelOptionsEditorBuilder, GrafanaTheme, dateTime } from \'@grafana/data\'; import { ColorPicker, Input, Icon, stylesFactory } from \'@grafana/ui\'; import { css } from \'emotion\'; import { config } from \'@grafana/runtime\'; import { ClockOptions, ClockMode, ClockType, FontWeight, ZoneFormat } from \'./types\'; import { getTimeZoneNames } from \'./ClockPanel\'; export const optionsBuilder = (builder: PanelOptionsEditorBuilder) => { // Global options builder //ClockModeの二者択一。TimeかCountdownを選ばせる。 .addRadio({ path: \'mode\', name: \'Mode\', settings: { options: [ { value: ClockMode.time, label: \'Time\' }, { value: ClockMode.countdown, label: \'Countdown\' }, ], }, defaultValue: ClockMode.time, }) //背景色のカスタムエディタ。カラーピッカーから色を選ばせる。 .addCustomEditor({ id: \'bgColor\', path: \'bgColor\', name: \'Background Color\', editor: props => { const styles = getStyles(config.theme); let prefix: React.ReactNode = null; let suffix: React.ReactNode = null; if (props.value) { suffix = props.onChange(undefined)} />; } prefix = ( ); return ( { console.log(\'CLICK\'); }} prefix={prefix} suffix={suffix} /> ); }, defaultValue: \'\', }); // TODO: refreshSettings.syncWithDashboard addCountdown(builder); addTimeFormat(builder); addTimeZone(builder); addDateFormat(builder); }; //--------------------------------------------------------------------- // COUNTDOWN //--------------------------------------------------------------------- function addCountdown(builder: PanelOptionsEditorBuilder) { const category = [\'Countdown\']; builder .addTextInput({ category, path: \'countdownSettings.endCountdownTime\', name: \'End Time\', settings: { placeholder: \'ISO 8601 or RFC 2822 Date time\', }, defaultValue: dateTime(Date.now()) .add(6, \'h\') .format(), showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.endText\', name: \'End Text\', defaultValue: \'00:00:00\', showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.customFormat\', name: \'Custom format\', settings: { placeholder: \'optional\', }, defaultValue: undefined, showIf: o => o.mode === ClockMode.countdown, }); } //--------------------------------------------------------------------- // TIME FORMAT //--------------------------------------------------------------------- function addTimeFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Time Format\']; builder .addRadio({ category, path: \'clockType\', name: \'Clock Type\', settings: { options: [ { value: ClockType.H24, label: \'24 Hour\' }, { value: ClockType.H12, label: \'12 Hour\' }, { value: ClockType.Custom, label: \'Custom\' }, ], }, defaultValue: ClockType.H24, }) .addTextInput({ category, path: \'timeSettings.customFormat\', name: \'Time Format\', description: \'the date formatting pattern\', settings: { placeholder: \'date format\', }, defaultValue: undefined, showIf: opts => opts.clockType === ClockType.Custom, }) .addTextInput({ category, path: \'timeSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'12px\', }) .addRadio({ category, path: \'timeSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, }); } //--------------------------------------------------------------------- // TIMEZONE //--------------------------------------------------------------------- function addTimeZone(builder: PanelOptionsEditorBuilder) { const category = [\'Timezone\']; const timezones = getTimeZoneNames().map(n => { return { label: n, value: n }; }); timezones.unshift({ label: \'Default\', value: \'\' }); builder .addSelect({ category, path: \'timezone\', name: \'Timezone\', settings: { options: timezones, }, defaultValue: \'\', }) .addBooleanSwitch({ category, path: \'timezoneSettings.showTimezone\', name: \'Show Timezone\', defaultValue: false, }) .addSelect({ category, path: \'timezoneSettings.zoneFormat\', name: \'Display Format\', settings: { options: [ { value: ZoneFormat.name, label: \'Normal\' }, { value: ZoneFormat.nameOffset, label: \'Name + Offset\' }, { value: ZoneFormat.offsetAbbv, label: \'Offset + Abbreviation\' }, { value: ZoneFormat.offset, label: \'Offset\' }, { value: ZoneFormat.abbv, label: \'Abbriviation\' }, ], }, defaultValue: ZoneFormat.offsetAbbv, showIf: s => s.timezoneSettings?.showTimezone, }) .addTextInput({ category, path: \'timezoneSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'font size\', }, defaultValue: \'12px\', showIf: s => s.timezoneSettings?.showTimezone, }) .addRadio({ category, path: \'timezoneSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.timezoneSettings?.showTimezone, }); } //--------------------------------------------------------------------- // DATE FORMAT //--------------------------------------------------------------------- function addDateFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Date Options\']; builder .addBooleanSwitch({ category, path: \'dateSettings.showDate\', name: \'Show Date\', defaultValue: false, }) .addTextInput({ category, path: \'dateSettings.dateFormat\', name: \'Date Format\', settings: { placeholder: \'Enter date format\', }, defaultValue: \'YYYY-MM-DD\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.locale\', name: \'Locale\', settings: { placeholder: \'Enter locale: de, fr, es, ... (default: en)\', }, defaultValue: \'\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'20px\', showIf: s => s.dateSettings?.showDate, }) .addRadio({ category, path: \'dateSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.dateSettings?.showDate, }); } const getStyles = stylesFactory((theme: GrafanaTheme) => { return { colorPicker: css` padding: 0 ${theme.spacing.sm}; `, inputPrefix: css` display: flex; align-items: center; `, trashIcon: css` color: ${theme.colors.textWeak}; cursor: pointer; &:hover { color: ${theme.colors.text}; } `, }; });