TECH PLAY

電通総研

電通総研 の技術ブログ

814

金融ソリューション事業部の橋詰です、こんにちは。 このポストは 電通国際情報サービス Advent Calendar 2021 の6日目のポストです。5日目の昨日は柴田さんの Policy as Codeを実現する Open Policy Agent / Rego の紹介 でした~。 さて、2021年も社内で複数の技術書読書会を開催しました。どんな本をどう読んだのかお披露目します。ISID社内には技術書の読書会がたくさんあるため、私が参加した会を中心にご紹介します。このテックブログに2021年の記録を残して、2022年も頑張ろうという気持ちを込めてのポストです。 読んだ本 読んだ本の一覧です。2020年からの継続と、今年仕掛りした本を含めています。合計6冊(6読書会)でした。 AIエンジニアを目指す人のための 機械学習入門 実装しながらアルゴリズムの流れを学ぶ システム設計の謎を解く 改訂版 強いSEになるための機能設計と入出力設計の極意 PyTorch実践入門 アジャイルなチームをつくる ふりかえりガイドブック 始め方・ふりかえりの型・手法・マインドセット ここはウォーターフォール市、アジャイル町 ストーリーで学ぶアジャイルな組織のつくり方 システム障害対応の教科書 それぞれの本と読み方を簡単にご紹介 本ごとに有志で集まって読んでいます。参加者はだいたい7-9人でしょうか。簡単に本の中身と感想、どんな読み方をしたのかまとめます。 AIエンジニアを目指す人のための 機械学習 入門 機械学習 のさまざまなモデルについて、 理論 を 簡単 に説明してくれる良書。「うわっつらの説明」と「難しい完全な理論の説明」のちょうど良い間をいってくれる、エンジニアにとってちょうどよい入門書です。本書は輪読形式で読みました。 システム設計の謎を解く システム開発 の基本設計に焦点を当てた本。設計を行う上で、これは最低限押さえておいてほしいなぁという粒度で記載されています。若手と一緒に読みました。この本も輪読形式です。 PyTorch実践入門 先述の「AIエンジニアを~」に続いて読んだ本。前半は 機械学習 ライブラリであるPyTorchの基礎、後半は CTスキャン 画像から肺がんを検出するタスクを題材にプロジェクトベースで理解を深めます。後半はけっこう難しい。しかしながら読書会というみんなで協力して(強制して!?)読む効果が発揮され、全員無事に読み切れました。こちらも輪読形式です。 ふりかえりガイドブック 社内の アジャイル 開発をしている人界隈で読もうとなった本。 システム開発 では必須のアクティビティである ふりかえり に焦点を当てています。この本は期日までに各々で読んで集合し、本書の内容や周辺の話題、現場の悩みをネタに、ディスカッション(リーンコーヒーなど)をする形式をとりました。同じ本を読み、コンテキストが揃っていると、普段仕事では関わらないメンバーとでも最初から良い議論ができるんだなと知った会でした。 ここ あじ ゃ本 組織の アジャイル 化のヒントを探すために、手に取った本。章ごとに小説調のストーリーと解説があり、とても読みやすいです。輪読会形式で読み進めたものの、読書する負担が少なく、他書と比べて多くのダイアログの時間を取ることができました。 システム障害対応の教科書 本書はただいま輪読会が進行中です。「できる人がやる」、「背中を見て育て!」と考えられていたシステム障害対応を、知識・タスク・ツール・組織の面から整理し教科書的にまとめ上げています。書籍の内容と、過去に経験した生々しい事例を共有・比較しながら進めていけるというのが、社内読書会を開催する大きなメリットですね。 さいごに 今年は、AI、 システム開発 、 アジャイル など、バリエーション豊かに読書会を開催できました。読み方が輪読会一辺倒になったところは、改善の余地がありますね。もう少し遊び心があると良いかもしれません。技術書でActive Book Dialogueを試すのも一興です。 さて、明日は読書会でもご一緒したことがある福竹さんの「ふりかえり入門した、ふりかえり」です。 執筆: @hashizume.hideki 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
金融ソリューション事業部の橋詰です、こんにちは。 このポストは 電通国際情報サービス Advent Calendar 2021 の6日目のポストです。5日目の昨日は柴田さんの Policy as Codeを実現する Open Policy Agent / Rego の紹介 でした~。 さて、2021年も社内で複数の技術書読書会を開催しました。どんな本をどう読んだのかお披露目します。ISID社内には技術書の読書会がたくさんあるため、私が参加した会を中心にご紹介します。このテックブログに2021年の記録を残して、2022年も頑張ろうという気持ちを込めてのポストです。 読んだ本 読んだ本の一覧です。2020年からの継続と、今年仕掛りした本を含めています。合計6冊(6読書会)でした。 AIエンジニアを目指す人のための 機械学習入門 実装しながらアルゴリズムの流れを学ぶ システム設計の謎を解く 改訂版 強いSEになるための機能設計と入出力設計の極意 PyTorch実践入門 アジャイルなチームをつくる ふりかえりガイドブック 始め方・ふりかえりの型・手法・マインドセット ここはウォーターフォール市、アジャイル町 ストーリーで学ぶアジャイルな組織のつくり方 システム障害対応の教科書 それぞれの本と読み方を簡単にご紹介 本ごとに有志で集まって読んでいます。参加者はだいたい7-9人でしょうか。簡単に本の中身と感想、どんな読み方をしたのかまとめます。 AIエンジニアを目指す人のための 機械学習 入門 機械学習 のさまざまなモデルについて、 理論 を 簡単 に説明してくれる良書。「うわっつらの説明」と「難しい完全な理論の説明」のちょうど良い間をいってくれる、エンジニアにとってちょうどよい入門書です。本書は輪読形式で読みました。 システム設計の謎を解く システム開発 の基本設計に焦点を当てた本。設計を行う上で、これは最低限押さえておいてほしいなぁという粒度で記載されています。若手と一緒に読みました。この本も輪読形式です。 PyTorch実践入門 先述の「AIエンジニアを~」に続いて読んだ本。前半は 機械学習 ライブラリであるPyTorchの基礎、後半は CTスキャン 画像から肺がんを検出するタスクを題材にプロジェクトベースで理解を深めます。後半はけっこう難しい。しかしながら読書会というみんなで協力して(強制して!?)読む効果が発揮され、全員無事に読み切れました。こちらも輪読形式です。 ふりかえりガイドブック 社内の アジャイル 開発をしている人界隈で読もうとなった本。 システム開発 では必須のアクティビティである ふりかえり に焦点を当てています。この本は期日までに各々で読んで集合し、本書の内容や周辺の話題、現場の悩みをネタに、ディスカッション(リーンコーヒーなど)をする形式をとりました。同じ本を読み、コンテキストが揃っていると、普段仕事では関わらないメンバーとでも最初から良い議論ができるんだなと知った会でした。 ここ あじ ゃ本 組織の アジャイル 化のヒントを探すために、手に取った本。章ごとに小説調のストーリーと解説があり、とても読みやすいです。輪読会形式で読み進めたものの、読書する負担が少なく、他書と比べて多くのダイアログの時間を取ることができました。 システム障害対応の教科書 本書はただいま輪読会が進行中です。「できる人がやる」、「背中を見て育て!」と考えられていたシステム障害対応を、知識・タスク・ツール・組織の面から整理し教科書的にまとめ上げています。書籍の内容と、過去に経験した生々しい事例を共有・比較しながら進めていけるというのが、社内読書会を開催する大きなメリットですね。 さいごに 今年は、AI、 システム開発 、 アジャイル など、バリエーション豊かに読書会を開催できました。読み方が輪読会一辺倒になったところは、改善の余地がありますね。もう少し遊び心があると良いかもしれません。技術書でActive Book Dialogueを試すのも一興です。 さて、明日は読書会でもご一緒したことがある福竹さんの「ふりかえり入門した、ふりかえり」です。 執筆: @hashizume.hideki 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
こんにちは、X イノベーション 本部の柴田です。 このポストは 電通国際情報サービス Advent Calendar 2021 の5日目のポストです。 4日目のポストは加納さんの「リアルタイムレンダラーP3Dのご紹介」でした。 さて、このポストではOpen Policy Agentとポリシー言語Regoの紹介をしたいと思います。 前半ではRegoの文法を簡単に説明します。 後半では私がOpen Policy AgentとRegoを実際に使っていてハマった点をいくつかご紹介します。 このポストを読んでくださる方の役に立てば幸いです。 Open Policy Agentとは Regoとは まずは動かしてみる 設問 構造化データ(input.json) ポリシー(example.rego) 検証 Regoの文法 本章で扱う構造化データ 変数 変数の束縛 配列、集合、オブジェクトへのアクセス ルール 集合を生成する オブジェクト(辞書)を生成する 値を生成する 複数の定義を書く 関数 内包表記 リスト内包表記 集合内包表記 オブジェクト内包表記 キーワードと演算子 not some default in with モジュール、パッケージ テストを書く OPA/Regoを利用するツール Conftest Gatekeeper KubernetesにおけるConftestとGatekeeperの使い分け それ以外のツール ハマったところ 矛盾したルールを定義しない 配列や集合がある条件を満たす値を含んでいないか検証するルールの書き方 オブジェクトのフィールドが特定の値であるか検証するルールの書き方 異常系テストのアサーションの書き方 オブジェクトの存在しないキーを参照した際の挙動 ポリシーのデバッグ方法 = と := と == の違い フォーマッタを使う おわりに 参考 Open Policy Agentとは Open Policy Agent(OPA)は汎用ポリシーエンジンです。 与えられた構造化データがRegoと呼ばれるポリシー言語で記述されたポリシーを満たしているか判定します。 OPAを使ってサービス本体とポリシーエンジンを 疎結合 にすることでポリシーの更新、デプロイ、バージョン管理などをサービス本体から分離できます。 ※図は Open Policy Agent | Documentation から引用しています。 OPAには様々な ユースケース があります。いくつか例を挙げます。 Infrastructure as Code : Infrastructure as Codeではインフラの理想的な状態を yaml やHCL2などの構造化データとして宣言的に記述します。 そしてそれをTerraformなどのツールへ渡すことで宣言に従って実際のインフラを構築します。 OPAを使って yaml やHCL2が満たすべきルール(=インフラの設定)をポリシーとして定義することでInfrastructure as Codeの品質を担保できます。 これは特に複数のチームが独自にサービスを構築・運用している場合にチーム横断で品質を向上する方法として有効です。 ログ分析 : OPAを使ってセキュリティや監査の構造化ログを分析し、指定したポリシーを満たさない異常なログが出力されていないかチェックできます。 たとえばメルカリでは GCP のセキュリティログをOPAを使って分析しています。 参考: Achieving Security Compliance Monitoring with Open Policy Agent and Rego - Speaker Deck データパイプライン上のバリデーション : OPAを使ってデータパイプライン上を流れる構造化データが指定したポリシーを満たしているかチェックできます。 たとえば 日本経済新聞社 ではデータパイプライン上のト ラッキング データが正常に加工されているかOPAでチェックしています。 参考: 【開催報告】新聞社による AWS を活用した DX 事例セミナー | Amazon Web Services ブログ OPAのソースコード は Apache License 2.0 で公開されています。 Regoとは RegoはOPAが使用する宣言型クエリ言語です。 Datalog( Prolog のサブセットである宣言型論理 プログラミング言語 )にインスパイアされています。 まずは動かしてみる 公式の Example に掲載されている例題を手元で実行してみます。 設問 与えられた構造化データが以下のポリシーを満たしているか検証します。 インターネットから到達可能なサーバのうち、安全でない http プロトコル を使用しているものはないか。 telnet プロトコル を使用しているサーバはないか。 構造化データ( input.json ) { " servers ": [ { " id ": " app ", " protocols ": [ " https ", " ssh " ] , " ports ": [ " p1 ", " p2 ", " p3 " ]} , { " id ": " db ", " protocols ": [ " mysql " ] , " ports ": [ " p3 " ]} , { " id ": " cache ", " protocols ": [ " memcache " ] , " ports ": [ " p3 " ]} , { " id ": " ci ", " protocols ": [ " http " ] , " ports ": [ " p1 ", " p2 " ]} , { " id ": " busybox ", " protocols ": [ " telnet " ] , " ports ": [ " p1 " ]} ] , " networks ": [ { " id ": " net1 ", " public ": " false " } , { " id ": " net2 ", " public ": " false " } , { " id ": " net3 ", " public ": " true " } , { " id ": " net4 ", " public ": " true " } ] , " ports ": [ { " id ": " p1 ", " network ": " net1 " } , { " id ": " p2 ", " network ": " net3 " } , { " id ": " p3 ", " network ": " net2 " } ] } ポリシー( example.rego ) 上述の設問を検証するためにRegoで記述されたポリシーを使用します。 ※Regoの文法は後ほど説明します。今はイメージだけ把握していただければ結構です。 package example # allow のデフォルト値を false に設定します default allow = false # violation の要素数が 0 の場合、 allow を true に設定します allow = true { count(violation) == 0 } # 生成した集合 public_server の要素 server のうち protocols が "http" である要素の id を集合 violation の要素に追加します violation[server.id] { some server public_server[server] server.protocols[_] == "http" } # 配列 input.servers の要素 server のうち protocols が "telnet" である要素の id を集合 violation の要素に追加します violation[server.id] { server := input.servers[_] server.protocols[_] == "telnet" } # 配列 input.servers の要素 server のうち以下の条件を満たす要素を集合 public_server の要素に追加します # 1. 配列 input.ports のうち server.ports のいずれかと id が合致する要素の添字を i とします # 2. 配列 input.networks のうち input.ports[i].network と id が合致する要素の添字を j とします # 3. input.networks[j].public が true である場合、 server を集合 public_server の要素に追加します public_server[server] { some i, j server := input.servers[_] server.ports[_] == input.ports[i].id input.ports[i].network == input.networks[j].id input.networks[j].public } 検証 ローカル環境で実行する場合は以下を行ってください。 open-policy-agent/opa から最新のリリースバイナリをダウンロードして、実行パスの通った ディレクト リに配置します。 opa eval コマンドでポリシーを評価します。 # 設問を満たしているか検証します。結果は "false" (満たしていない)でした。 $ opa eval -i input.json -d policy.rego --format pretty data.example.allow false # 設問のポリシーを満たしていないサーバは "ci","busybox" であることが確認できます。 $ opa eval -i input.json -d policy.rego --format pretty data.example.violation [ "ci", "busybox" ] または The Rego Playground からも実行できます。 Regoの文法 Regoの文法を簡単に説明します。 本章で扱う構造化データ 本章( Regoの文法 )では以下の構造化データをサンプルデータとして使用します。 { " persons ": [ { " name ": " alice ", " age ": 20 , " height ": 170 , " weight ": 60 } , { " name ": " bob ", " age ": 22 , " height ": 180 , " weight ": 80 } , { " name ": " carol ", " age ": 17 , " height ": 175 , " weight ": 66 } , { " name ": " dave ", " age ": 18 , " height ": 155 , " weight ": 50 } ] } 変数 変数の束縛 Regoの変数は「指定された条件を満たすよう束縛された値」です。 例えば以下の場合、変数 i は 配列 persons のうち「20才以下かつ身長170cm以上の人」を指す任意の添字 の値、つまり 0 と 2 に束縛されます。 some i persons[i].age <= 20 persons[i].height >= 170 手続き型 プログラミング言語 のように値を再び代入することはできません。 i := 1 i := 2 # i は既に 1 に束縛にされているためエラーになる また特殊な変数として _ があります。 _ は登場する度に他の変数と競合しない一意な変数へ変換されます。再び参照する必要のない変数は _ を使うことが推奨されています。 _ を使って先程の例を書き換えると以下のようになります。 person := persons[_] person.age <= 20 person.height >= 160 配列、集合、オブジェクトへのアクセス Regoには以下のような型があります。 数値 文字列 ブール値 配列 集合 オブジェクト(辞書) 配列 arr から要素 val を取りだす際は以下のように書きます。 # 変数を使った書き方。iは配列の添字、valは配列の値に束縛される。 some i val := arr[i] # または `_` を使った書き方。 val := arr[_] 集合 a_set から要素 val を取りだす際は以下のように書きます。 # valは集合の各要素に束縛される a_set[val] オブジェクト obj のキー key にアクセスする際は以下のように書きます。 # `.` を使った書き方 value := obj.key # または `[]` を使った書き方 value := obj["key"] ルール OPAでは、渡された構造化データ(ベースドキュメントとも言います)を元に、新しい構造化データ(仮想ドキュメントとも言います)を算出します。 この仮想ドキュメントの定義を ルール と言います。OPAでは様々なルールを定義することでポリシーを実装します。 集合を生成する 集合を生成するルールは以下のように記述します。 <name>[<value>] { <body> } このときルール <name> は「 <body> の式が全て真になるときの <value> 」を要素として持つ集合になります。 <body> の各式は 論理積 (AND)で結合されます。 以下は 「年齢20才以下、身長170cm以上の人」の名前の集合 を生成するルールの例です。 young_and_tall_persons[person.name] { person := input.persons[_] person.age <= 20 person.height >= 170 } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func youngAndTallPersons(input Input) [] string { result := [] string {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result = append (result, person.name) } } return result } オブジェクト(辞書)を生成する オブジェクトを生成するルールは以下のように記述します。 <name>[<key>] = <value> { <body> } このときルール <name> は「 <body> の式が全て真になるときの <key> と <value> のペア」を要素として持つオブジェクトになります。 <body> の各式は 論理積 (AND)で結合されます。 以下は 「年齢20才以下、身長170cm以上の人」について、名前と体重のペアを要素として持つのオブジェクト を生成するルールの例です。 young_and_tall_persons_weight[person.name] = person.weight { person := input.persons[_] person.age <= 20 person.height >= 170 } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func youngAndTallPersonsWeight(input Input) map [ string ] int { result := map [ string ] int {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result[person.name] = person.weight } } return result } 値を生成する 値を生成するルールは以下のように記述します。 <name> = <value> { <body> } このときルール <name> は「 <body> の式が全て真になるときの <value> の値」になります。 <body> の各式は 論理積 (AND)で結合されます。 以下は「Aliceの身長」を返すルールの例です。 alice_height = person.height { person := input.persons[_] person.name == "alice" } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func aliceHeight(input Input) int { result := 0 for _, person := range input.persons { if person.name == "alice" { result = person.height } } return result } <vaule> には数値、文字列、真偽値だけでなく、集合、配列、オブジェクトなども指定できます。 = <value> 部を省略すると = true として扱われます。 例えば以下のルール alice_exists は name キーの値が alice な要素が存在する場合のみ true になります。 alice_exists { person := input.persons[_] person.name == "alice" } { <body> } 部を省略すると { true } として扱われます。定数はこの書き方を使って定義します。 pi = 3.14 複数の定義を書く ルールは複数に分割して定義できます。 分割して記述されたルールは 論理和 (OR)のように評価されます。 以下は 「 BMI が18.5未満または25以上の人」の名前の集合 を返すルールの例です。 unhealthy_persons[person.name] { person := input.persons[_] bmi := (person.weight / (person.height * person.height)) * 10000 bmi < 18.5 } unhealthy_persons[person.name] { person := input.persons[_] bmi := (person.weight / (person.height * person.height)) * 10000 bmi >= 25 } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func unhealthyPersons(input Input) [] string { result := [] string {} for _, person := range input.persons { bmi := float64 (person.weight) / ( float64 (person.height) * float64 (person.height)) * 10000.0 if bmi < 18.5 || bmi >= 25 { result = append (result, person.name) } } return result } 関数 関数は以下のように記述します。引数を取ること以外は 値を生成するルール と概ね同じです。 <name> (<args>) = <value> { <body> } 以下は BMI を計算する関数と、 BMI の分類を返す関数の例です。 bmi(height, weight) = (weight / (height * height)) * 10000 bmi_class(person) = "underweight" { bmi(person.height, person.weight) < 18.5 } bmi_class(person) = "normal range" { bmi(person.height, person.weight) >= 18.5 bmi(person.height, person.weight) < 25 } bmi_class(person) = "overweight" { bmi(person.height, person.weight) >= 25 } またOPA/Regoには組込み関数があります。よく使う組込み関数をいくつか紹介します。 # 書式付き文字列を評価して結果の文字列を返す sprintf("%s's weight is %d", [person.name, person.weight]) # 配列、集合、オブジェクトの要素数を返す count(young_and_tall_persons) == 0 # オブジェクトを文字列(json形式)に変換したものを返す json.marshal(obj) # --explainオプションをつけて実行した際に文字列を出力する(詳細は後述) trace("...") 上で紹介した以外にも様々な組込み関数があります。詳細は Built-in Functions を参照してください。 内包表記 Regoでは内包表記を使用できます。 リスト内包表記 以下は 「年齢20才以下、身長170cm以上の人」の名前の配列 を生成する内包表記の例です。 array := [person.name | person := input.persons[_]; person.age <= 20; person.height >= 170] これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 array := func (input Input) [] string { result := [] string {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result = append (result, person.name) } } return result }(input) 集合内包表記 以下は 集合を生成する で例示した 「年齢20才以下、身長170cm以上の人」の名前の集合 を内包表記で記述した例です。 a_set := {person.name | person := input.persons[_]; person.age <= 20; person.height >= 170} これをGoで書いた場合のコードは リスト内包表記 と同じため割愛します(※あくまでイメージです)。 オブジェクト内包表記 以下は オブジェクト(辞書)を生成する で例示した 「年齢20才以下、身長170cm以上の人」について、名前と体重のペアを要素として持つのオブジェクト を内包表記で記述した例です。 obj := {person.name: person.weight | person := input.persons[_]; person.age <= 20; person.height >= 170} これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 obj := func (input Input) map [ string ] int { result := map [ string ] int {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result[person.name] = person.weight } } return result }(input) キーワードと 演算子 not 式を否定するには not を使用します。 not person.age <= 20 ルールが仮想ドキュメントを生成したか 配列や集合やオブジェクトの中に指定した要素が存在するか を判定する際にも使用します。 not input.persons[4] not person.birthday some some を使うことでルール内のローカル変数を明示的に宣言できます。 young_and_tall_persons[person.name] { some i person := input.persons[i] person.age <= 20 person.height >= 170 } ローカル変数の宣言を必ず行う必要はありませんが、変数名と同じルールが存在した場合でも期待通りローカル変数として扱われるよう、明示的に宣言することが推奨されています。 例えば以下のルール young_and_tall_persons は some i の有無で結果が変わります。 i = 1 young_and_tall_persons[person.name] { some i person := input.persons[i] person.age <= 20 person.height >= 170 } default default を使うことで 値を生成するルール のデフォルト値を指定できます。 以下の例では、ルール alice_exists は input.persons の中にAliceの要素が存在すれば true 、そうでなければ false になります。 default キーワードを省略すると、ルール alice_exists は input.persons の中にAliceの要素が存在しない場合 false ではなく未定義になります。 default alice_exists = false alice_exists { person := input.persons[_] person.name == "alice" } in in はOPAの v0.34.0 で導入されたキーワードです。 配列、集合、オブジェクトがある値を要素に含んでいるか(または含んでいないか)判定する 配列、集合、オブジェクトの要素に変数を束縛する といったことができます。 OPA v0.34.2 時点では、 in キーワードを使うには import future.keywords.in を宣言する必要があります。 以下は配列、集合、オブジェクトがある値を要素に含んでいるか判定するルールの例です。 import future.keywords.in # 名前の集合。結果は ["alice", "bob", "carol", "dave"] 。 person_names[person.name] { person := input.persons[_] } # 名前と体重のペアからなるオブジェクト。結果は {"alice": 60, "bob": 80, "carol": 66, "dave": 50} 。 person_weights[person.name] = person.weight { person := input.persons[_] } # 集合 person_names の中に "alice" が存在するか。結果は true 。 alice_exists { "alice" in person_names } # オブジェクト person_weights の中に値が 60 である要素(例: {"alice": 60} )が存在するか。結果は true 。 sixty_kg_person_exists { 60 in person_weights } not と組み合わせて、配列、集合、オブジェクトがある値を要素に含んでいないかを判定できます。 # 集合 person_names の中に "ellen" が存在しないか。結果は true 。 ellen_does_not_exist { not "ellen" in person_names } # オブジェクト person_weights の中に値が 70 である要素(例: {"alice": 70} )が存在しないか。結果は true 。 seventy_kg_person_does_not_exist { not 70 in person_weights } 左辺に引数を2つ渡すことで、配列、オブジェクトが「添字、キー」と「値」のペアを含んでいるかを判定できます。 # オブジェクト person_weights の中に要素 {"alice": 60} が存在するか。結果は true 。 is_alice_sixty_kg { "alice", 60 in person_weights } 配列、集合の定義 関数の引数 などで in キーワードの左辺に引数を2つ渡す場合は、正しく評価されるよう () で囲む必要があります。 # 誤った書き方。「`0`と `2 in [2]` の2つの引数を受け取る関数f」として解釈される。 f(0, 2 in [2]) # 正しい書き方。「`0, 2 in [2]` の結果を1つの引数として受け取る関数f」として解釈される。 f((0, 2 in [2])) some と組み合わせて、配列、集合、オブジェクトの要素に変数を束縛できます。 # 体重が60kgの人の名前の集合を返す。結果は {"alice"} 。 sixty_kg_persons[name] { some name, 60 in person_weights } # 結果は ["a", "r", "y"] unique[x] { some x in ["a", "r", "r", "a", "y"] } with ルールの評価時に with を使って input data.<path> の構造化データを指定した値に置き換えることができます。 allow with input as {"user": "charlie", "method": "GET"} with data.roles as {"dev": ["charlie"]} これは主に 単体テスト を書く際に使用します。テストの書き方は テストを書く で説明します。 モジュール、パッケージ 0個以上のルールの集合をモジュールといい、それを特定の 名前空間 にグループ化したものをパッケージといいます。 同じパッケージのポリシーを同じ ディレクト リに配置する必要はありません。 パッケージ名は package で指定します。 package example 他のパッケージのルールや関数を参照する場合は import data.<パッケージ名> のように宣言します。 import data.other_example example_rule[value] { value := other_example.other_rule } テストを書く 単体テスト を書くことでポリシーが期待通り動作するか確認できます。 テストコードはファイル名の末尾を _test.rego としてください。 例えば example.rego のテストコードのファイル名は example_test.rego です。 テストケースは名前が test_ から始まるルールとして記述します。ルールのbody部の式が全て真ならば成功、そうでなければ失敗です。 package example test_young_and_tall_persons { input := {"persons": [ {"name": "alice", "age": 20, "height": 170, "weight": 60}, {"name": "bob", "age": 22, "height": 180, "weight": 80}, {"name": "carol", "age": 17, "height": 175, "weight": 66}, {"name": "dave", "age": 18, "height": 155, "weight": 50}, ]} young_and_tall_persons == {"alice", "carol"} with input as input } test_no_young_and_tall_persons { input := {"persons": [ {"name": "alice", "age": 30, "height": 170, "weight": 60}, {"name": "bob", "age": 22, "height": 180, "weight": 80}, {"name": "carol", "age": 27, "height": 175, "weight": 66}, {"name": "dave", "age": 18, "height": 155, "weight": 50}, ]} count(young_and_tall_persons) == 0 with input as input } テストは以下のように実行します。 $ opa test . PASS: 2/2 試しにテストケース test_young_and_tall_persons の アサーション の右辺を {"alice", "carol"} から {"alice", "bob"} へ書き換えてテストを失敗させます。 テストが失敗した場合は次のような結果になります。 $ opa test . data.example.test_young_and_tall_persons: FAIL (1.466812ms) -------------------------------------------------------------------------------- PASS: 1/2 FAIL: 1/2 opa test コマンドには以下のようなオプションもあります。 -v, --verbose :テスト結果の詳細を表示します。 -r, --run :指定したテストケースのみを実行できます。 -c, --coverate :テスト カバレッジ を出力できます。 OPA/Regoを利用するツール OPA/Regoを利用するツールを簡単にご紹介します。 Conftest Conftest は Kubernetes の マニフェスト ファイルやTerraformコードなどの構成ファイルがRegoで記述されたポリシーに従っているか検証するための CLI ツールです。 以下は Kubernetes のPodに Readiness Probe が設定されているか検証するポリシーの例です(※このポリシーは説明のために簡略化しており実運用には適していません)。 package deny_container_without_readiness_probe violation[msg] { input.kind == "Pod" container := input.spec.containers[_] not container.readinessProbe msg := "Readiness Probe must be set" } 先程のポリシーを用いて以下の マニフェスト ファイルを検証してみます。 apiVersion : v1 kind : Pod metadata : name : myapp spec : containers : - name : myapp image : myapp:1.0.0 ports : - containerPort : 8080 $ conftest test --policy . --all-namespaces manifests.yaml FAIL - manifests.yaml - deny_container_without_readiness_probe - Readiness Probe must be set 1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions マニフェスト がポリシーに違反していることを検出できました。 Conftestは Kubernetes の マニフェスト ファイル以外の構成ファイルに対しても利用できます。公式ドキュメントの Examples には以下の構成ファイルを検証するサンプルポリシーが掲載されています。 AWS SAM Framework CUE Docker compose Dockerfile EDN Ignore HCL HCL 2 HOCON INI Jsonnet Kubernetes Kustomize Serverless Framework Traefik Typescript VCL XML Gatekeeper Gatekeeper は Kubernetes の Validating Admission Webhook として動作します。 kube-apiserverに対するリソース作成の要求を検証し、ポリシーに違反するリソースの作成を禁止できます。 Kubernetes におけるConftestと Gatekeeper の使い分け Conftestは主に開発者のローカル環境やCIサーバなどで実行されます。 git リポジトリ 上の マニフェスト ファイルが変更された際に、その変更がポリシーに従っているか検証し、即座に開発者へフィードバックできます。 ただし以下のようなケースはConftestで検知できません。 オペレーターが kubectl create などで Kubernetes クラスタ ーにリソースを直接作成した場合 Kubernetes クラスタ ー上のCustomControllerによってリソースが作成された場合 Gatekeeper は Kubernetes のValidating Admission Webhookとして動作します。 kube-apiserverに対する全てのリソース作成の要求を検証し、ポリシーに違反するリソースの作成を禁止できます。 ただし kubectl create や kubectl apply を実行して実際にkube-apiserverへリク エス トを送信するまで、ポリシーに違反しているか検証できません。 Conftestと Gatekeeper はどちらか片方だけ使うのではなく、両方組み合わせて使うのがよいでしょう。 主な実行場所 主な実行タイミング 検証対象 Conftest 開発者のローカル環境、CIサーバ マニフェスト ファイルの更新時 マニフェスト ファイル Gatekeeper Kubernetes のValidating Admission Webhook kube-apiserverへのリソース作成の要求時 kube-apiserverに対する全てのリソース作成の要求 Conftestも Gatekeeper もRegoで記述されたポリシーを使用します。 しかし構造化データ(= マニフェスト )の渡し方などに差異があります。 Conftestと Gatekeeper で同じポリシーを使用する場合、何らかの方法でこの差分を吸収する必要があります。 マニフェスト の格納場所 Conftest( --combine オプションなし)(*1) input Conftest( --combine オプションあり)(*1) input[_].contents Gatekeeper input.review.object (*1):Conftestは通常 マニフェスト ファイルに含まれるリソースを1つずつ個別に評価します。 そのため、例えばDeploymentに対してPodDisruptionBudgetが定義されているかといったような、複数のリソースにまたがったポリシーを評価できません。 --combine オプションを設定することで、 マニフェスト ファイルに含まれる全てのリソースを同時にRegoへ渡し、複数のリソースにまたがったポリシーを評価できます。 またConftestが使用する *.rego ファイルから Gatekeeper が必要とするリソース( ConstraintTemplate など)を生成する必要があります。 これを行うツールに konstraint があります。 それ以外のツール これまで紹介したツールの他に、以下のツールもRegoで記述されたポリシーを利用しています。 Trivy Terrascan ハマったところ 私が実際にOPA/Regoを使っていてハマった点をいくつかご紹介します。 矛盾したルールを定義しない ルールの定義が矛盾してはいけません。そのようなルールに矛盾を発生させる構造化データを渡すと実行時エラーが発生します。 いくつか悪い例を紹介します。 以下は値を生成するルールです。ただし値が1つに定まりません。 height = person.height { person = input.persons[_] } 以下の例ではルールの定義が複数に分かれています。 1つ目の定義では「 name と weight のペアを要素として持つオブジェクト」を生成します。 一方、2つ目の定義では「 name と height のペアを要素として持つオブジェクト」を生成します。 結果、生成されたオブジェクトの name キーの値は1つに定まりません。 personal_health_info[person.name] = person.height { person := input.persons[_] } personal_health_info[person.name] = person.weight { person := input.persons[_] } 配列や集合がある条件を満たす値を含んでいないか検証するルールの書き方 配列 array や集合 a_set が関数 f を満たす値を含まないか判定するルールは以下のように記述できます。 # 配列が関数fを満たす値を含んでいないか none_in_array_match { count({x | x := array[_]; f(x)}) == 0 } # 集合が関数fを満たす値を含んでいないか none_in_set_match { count({x | a_set[x]; f(x)}) == 0 } 特に配列 array や集合 a_set がある値 x を要素に含んでいないか判定したい場合は in を使って以下のように記述できます。 ※OPA v0.34.2 時点では、 in キーワードを使うには import future.keywords.in を宣言する必要があります。 import future.keywords.in # 配列がある値を含んでいないか none_in_array_match { not x in array } # 集合がある値を含んでいないか none_in_set_match { not x in a_set } 以下の書き方は誤りです。 以下のルールは配列や集合に関数 f を満たさない値が1つでも存在すれば true になります。 配列や集合の全ての要素が関数 f を満たさないことは検証できません。 # 配列が関数fを満たす値を含んでいないか(間違った書き方) none_in_array_match { x := array[_] not f(x) } # 集合が関数fを満たす値を含んでいないか(間違った書き方) none_in_set_match { a_set[x] not f(x) } オブジェクトのフィールドが特定の値であるか検証するルールの書き方 オブジェクト obj のキー key の値が value であることを判定するルールは以下のように記述できます。 violation[msg] { not has_specific_value(obj) msg := "'obj.key' must be 'value'" } has_specific_value(obj) { obj.key == value } 以下の書き方は誤りです。 「オブジェクト obj のキー key の値が value である場合」に加えて「オブジェクト obj にキー key が存在しない場合」にもルールのbody部が false になるためです。 violation[msg] { obj.key != value msg := "'obj.key' must be 'value'" } 異常系テストの アサーション の書き方 以下は入力データの値が偶数かどうか判定するポリシーの例です。 violation[msg] { input.value % 2 != 0 msg := "Value must be even" } 上述のポリシーに対する異常系テストとして、最初私は以下のようなテストケースを記述していました。 test_odd { input := {"value": 1} violation with input as input } ですが上述のテストケースは下表の通り常に成功してしまいます。 これは {} ( 空集合 )が真として扱われるためです。 よってこのテストケースの書き方は誤りです。 value の値 violation の結果 test_odd の結果 奇数(例:1, 3, ...) {"Value must be even"} true 偶数(例:2, 4, ...) {} ( 空集合 ) true 参考: open policy agent - Rego testing: how to test "not deny"? - Stack Overflow 正しくは以下のいずれかのように記述するとよいでしょう。上の方がより厳密に検証を行います。 # 例1:violationが指定した値に完全に一致するか確認する。 test_odd { input := {"value": 1} violation == {"Value must be even"} with input as input } # 例2:violationに指定した要素が含まれているか確認する。他の要素が含まれている可能性もある。 test_odd { input := {"value": 1} violation["Value must be even"] with input as input } # 例3:violationが空でないことを確認する。具体的な要素までは確認しない。 test_odd { input := {"value": 1} count(violation) > 0 with input as input } オブジェクトの存在しないキーを参照した際の挙動 オブジェクトの存在しないキーを参照するとルールが意図しない結果を返すため注意が必要です。 先程の偶数かどうか判定するポリシーを誤って以下のように記述したとします。 violation[msg] { input.number % 2 != 0 # 値が格納されているのは input.value だが誤って input.number を参照している msg := "Value must be even" } このとき input.value がどのような値でも violation は 空集合 を返します。 これはルール violation のbody部における1番目の式が、 input.value の値にかかわらず、常に未定義(つまり真ではない)になるためです。 $ cat input_odd.json { "value": 1 } $ opa eval -i input_odd.json -d policy.rego --format pretty data.example.violation [] $ cat input_even.json { "value": 2 } $ opa eval -i input_even.json -d policy.rego --format pretty data.example.violation [] ここで問題なのは未定義が false と同様に扱われていることです。 ルールの中でオブジェクトの存在しないキーを参照して未定義が発生しても特に例外などは発生しません。 そのため正しくポリシーが評価されてルールのbody部が false になったのか、オブジェクトの存在しないキーを参照して未定義になったのか、区別するのは困難です。 現時点では、 単体テスト の カバレッジ を上げる以外に、この問題を解決するよい方法は見つけられていません。 またこれは「OPAのポリシーエンジン」と「それを呼びだす外部サービス」のインターフェース部分の仕様が変更された場合も問題になります。 例えばConftestでは v0.22.0 で --combine オプションをつけた際にRegoへ渡すデータの構造が変更されました( #388 )。 結果、いくつかのルールがオブジェクトの存在しないキーを参照して必ず未定義になり、 マニフェスト がポリシーに違反していてもConftestの検証が常に成功し続けるという問題が起こりました。 これを防ぐ方法として、Regoの 単体テスト に加えて「OPAのポリシーエンジン」と「それを呼びだす外部サービス」の間の 結合テスト を実装することが挙げられます。 例えば私はConftestのテスト結果を conftest test -o json で出力しておき、Conftestをバージョンアップする際はテスト結果に意図しない変更が発生していないか確認するスナップショットテストを行うようにしています。 ポリシーの デバッグ 方法 ポリシーの結果が期待通りでない場合、原因を調査する必要があります。ですがRegoの処理の流れは複雑で追うのが困難です。 ポリシーの挙動を把握する方法として --explain オプションを活用できます。 例として以下のポリシーと構造化データを --explain オプションをつけて評価します。 violation[msg] { trace(sprintf("input.value is %d", [input.value])) input.value % 2 != 0 msg := "Value must be even" } { " value ": 2 } --explain オプションの値とそれに対する出力は以下のとおりです。 --explain=full :ルールがどのように評価されたか全て表示する。 $ opa eval -i input.json -d policy.rego --explain full --format pretty data.example.violation query:1 Enter data.example.violation = _ query:1 | Eval data.example.violation = _ query:1 | Index data.example.violation (matched 1 rule) policy.rego:3 | Enter data.example.violation policy.rego:4 | | Eval __local3__ = input.value policy.rego:4 | | Eval sprintf("input.value is %d", [__local3__], __local1__) policy.rego:4 | | Eval trace(__local1__) policy.rego:4 | | Note "input.value is 2" policy.rego:5 | | Eval __local4__ = input.value policy.rego:5 | | Eval rem(__local4__, 2, __local2__) policy.rego:5 | | Eval neq(__local2__, 0) policy.rego:5 | | Fail neq(__local2__, 0) policy.rego:5 | | Redo rem(__local4__, 2, __local2__) policy.rego:5 | | Redo __local4__ = input.value policy.rego:4 | | Redo trace(__local1__) policy.rego:4 | | Redo sprintf("input.value is %d", [__local3__], __local1__) policy.rego:4 | | Redo __local3__ = input.value query:1 | Exit data.example.violation = _ query:1 Redo data.example.violation = _ query:1 | Redo data.example.violation = _ [] --explain=fails :式が false になった部分のみ表示する。 $ opa eval -i input.json -d policy.rego --explain fails --format pretty data.example.violation query:1 Enter data.example.violation = _ policy.rego:3 | Enter data.example.violation policy.rego:5 | | Fail neq(__local2__, 0) [] --explain=notes : trace 関数(後述)の内容のみ表示する。 $ opa eval -i input.json -d policy.rego --explain notes --format pretty data.example.violation query:1 Enter data.example.violation = _ policy.rego:3 | Enter data.example.violation policy.rego:4 | | Note "input.value is 2" [] trace 関数を使うと --explain オプションをつけて実行した際に文字列を出力できます。 trace 関数の引数は文字列ですが sprintf 関数や json.marshal 関数と組み合わせることで文字列以外の値(数値やオブジェクトなど)も出力できます。 これを使ってprint デバッグ できます。 --explain=full の出力を読み解くのはなかなか大変なので、最初は trace 関数と --explain=notes オプションを組み合わせてprint デバッグ するのがよいでしょう。 Conftestでも --trace オプションをつけることでルールがどのように評価されたか表示できます。これは opa コマンドの --explain=full に相当します。 = と := と == の違い Regoには = 、 := 、 == という似たような 演算子 があります。それぞれの違いは以下のとおりです。 記法 記述できる場所 コンパイル エラー ユースケース := ルール内 変数が既に定義されている場合に発生 ローカル変数を定義する == ルール内 変数がまだ定義されていない場合に発生 値を比較する = どこでも 変数が正しく参照できない場合に発生 クエリを表現する := と == のかわりに = を使うことができます。 ただし := と == は コンパイル 時に追加のチェックが働くため、使える場面ではなるべく = よりも := と == を使うとよいでしょう。 参考: Equality: Assignment, Comparison, and Unification フォーマッタを使う opa fmt --write . で現在の ディレクト リ配下にあるポリシーのフォーマットを整えることができます。 複数人でポリシーを更新する際はこのコマンドを使ってインデントや改行の有無などを統一するとよいでしょう。 また opa fmt --fail . でポリシーのフォーマットが整っていなかった場合に0以外の終了コードを返すことができます。 CIに組み込んでポリシーのフォーマットが整っているか検証するとよいでしょう。 おわりに このポストではOpen Policy Agentとポリシー言語Regoの紹介をしました。 前半ではRegoの文法を簡単に説明しました。 後半では私がOpen Policy AgentとRegoを実際に使っていてハマった点をいくつかご紹介しました。 このポストが読んでくださった方の役に立てば幸いです。 さて、明日は橋詰さんの「今年社内の技術書読書会で読んだ本!」です。 どんな技術書が紹介されるのか楽しみです! 最後までお読みいただきありがとうございました。 参考 Open Policy Agent | Documentation 特に Policy Reference はRegoの色々な書き方が紹介されていて参考になります。 Open Policy Agent Rego Knowledge Sharing Meetup - connpass Achieving Security Compliance Monitoring with Open Policy Agent and Rego - Speaker Deck 【開催報告】新聞社による AWS を活用した DX 事例セミナー | Amazon Web Services ブログ Gatekeeper/conftestのRegoをDRYに管理する — HACK The Nikkei 執筆: @shibata.takao 、レビュー: @ueba.yuki ( Shodo で執筆されました )
アバター
こんにちは、X イノベーション 本部の柴田です。 このポストは 電通国際情報サービス Advent Calendar 2021 の5日目のポストです。 4日目のポストは加納さんの「リアルタイムレンダラーP3Dのご紹介」でした。 さて、このポストではOpen Policy Agentとポリシー言語Regoの紹介をしたいと思います。 前半ではRegoの文法を簡単に説明します。 後半では私がOpen Policy AgentとRegoを実際に使っていてハマった点をいくつかご紹介します。 このポストを読んでくださる方の役に立てば幸いです。 Open Policy Agentとは Regoとは まずは動かしてみる 設問 構造化データ(input.json) ポリシー(example.rego) 検証 Regoの文法 本章で扱う構造化データ 変数 変数の束縛 配列、集合、オブジェクトへのアクセス ルール 集合を生成する オブジェクト(辞書)を生成する 値を生成する 複数の定義を書く 関数 内包表記 リスト内包表記 集合内包表記 オブジェクト内包表記 キーワードと演算子 not some default in with モジュール、パッケージ テストを書く OPA/Regoを利用するツール Conftest Gatekeeper KubernetesにおけるConftestとGatekeeperの使い分け それ以外のツール ハマったところ 矛盾したルールを定義しない 配列や集合がある条件を満たす値を含んでいないか検証するルールの書き方 オブジェクトのフィールドが特定の値であるか検証するルールの書き方 異常系テストのアサーションの書き方 オブジェクトの存在しないキーを参照した際の挙動 ポリシーのデバッグ方法 = と := と == の違い フォーマッタを使う おわりに 参考 Open Policy Agentとは Open Policy Agent(OPA)は汎用ポリシーエンジンです。 与えられた構造化データがRegoと呼ばれるポリシー言語で記述されたポリシーを満たしているか判定します。 OPAを使ってサービス本体とポリシーエンジンを 疎結合 にすることでポリシーの更新、デプロイ、バージョン管理などをサービス本体から分離できます。 ※図は Open Policy Agent | Documentation から引用しています。 OPAには様々な ユースケース があります。いくつか例を挙げます。 Infrastructure as Code : Infrastructure as Codeではインフラの理想的な状態を yaml やHCL2などの構造化データとして宣言的に記述します。 そしてそれをTerraformなどのツールへ渡すことで宣言に従って実際のインフラを構築します。 OPAを使って yaml やHCL2が満たすべきルール(=インフラの設定)をポリシーとして定義することでInfrastructure as Codeの品質を担保できます。 これは特に複数のチームが独自にサービスを構築・運用している場合にチーム横断で品質を向上する方法として有効です。 ログ分析 : OPAを使ってセキュリティや監査の構造化ログを分析し、指定したポリシーを満たさない異常なログが出力されていないかチェックできます。 たとえばメルカリでは GCP のセキュリティログをOPAを使って分析しています。 参考: Achieving Security Compliance Monitoring with Open Policy Agent and Rego - Speaker Deck データパイプライン上のバリデーション : OPAを使ってデータパイプライン上を流れる構造化データが指定したポリシーを満たしているかチェックできます。 たとえば 日本経済新聞社 ではデータパイプライン上のト ラッキング データが正常に加工されているかOPAでチェックしています。 参考: 【開催報告】新聞社による AWS を活用した DX 事例セミナー | Amazon Web Services ブログ OPAのソースコード は Apache License 2.0 で公開されています。 Regoとは RegoはOPAが使用する宣言型クエリ言語です。 Datalog( Prolog のサブセットである宣言型論理 プログラミング言語 )にインスパイアされています。 まずは動かしてみる 公式の Example に掲載されている例題を手元で実行してみます。 設問 与えられた構造化データが以下のポリシーを満たしているか検証します。 インターネットから到達可能なサーバのうち、安全でない http プロトコル を使用しているものはないか。 telnet プロトコル を使用しているサーバはないか。 構造化データ( input.json ) { " servers ": [ { " id ": " app ", " protocols ": [ " https ", " ssh " ] , " ports ": [ " p1 ", " p2 ", " p3 " ]} , { " id ": " db ", " protocols ": [ " mysql " ] , " ports ": [ " p3 " ]} , { " id ": " cache ", " protocols ": [ " memcache " ] , " ports ": [ " p3 " ]} , { " id ": " ci ", " protocols ": [ " http " ] , " ports ": [ " p1 ", " p2 " ]} , { " id ": " busybox ", " protocols ": [ " telnet " ] , " ports ": [ " p1 " ]} ] , " networks ": [ { " id ": " net1 ", " public ": " false " } , { " id ": " net2 ", " public ": " false " } , { " id ": " net3 ", " public ": " true " } , { " id ": " net4 ", " public ": " true " } ] , " ports ": [ { " id ": " p1 ", " network ": " net1 " } , { " id ": " p2 ", " network ": " net3 " } , { " id ": " p3 ", " network ": " net2 " } ] } ポリシー( example.rego ) 上述の設問を検証するためにRegoで記述されたポリシーを使用します。 ※Regoの文法は後ほど説明します。今はイメージだけ把握していただければ結構です。 package example # allow のデフォルト値を false に設定します default allow = false # violation の要素数が 0 の場合、 allow を true に設定します allow = true { count(violation) == 0 } # 生成した集合 public_server の要素 server のうち protocols が "http" である要素の id を集合 violation の要素に追加します violation[server.id] { some server public_server[server] server.protocols[_] == "http" } # 配列 input.servers の要素 server のうち protocols が "telnet" である要素の id を集合 violation の要素に追加します violation[server.id] { server := input.servers[_] server.protocols[_] == "telnet" } # 配列 input.servers の要素 server のうち以下の条件を満たす要素を集合 public_server の要素に追加します # 1. 配列 input.ports のうち server.ports のいずれかと id が合致する要素の添字を i とします # 2. 配列 input.networks のうち input.ports[i].network と id が合致する要素の添字を j とします # 3. input.networks[j].public が true である場合、 server を集合 public_server の要素に追加します public_server[server] { some i, j server := input.servers[_] server.ports[_] == input.ports[i].id input.ports[i].network == input.networks[j].id input.networks[j].public } 検証 ローカル環境で実行する場合は以下を行ってください。 open-policy-agent/opa から最新のリリースバイナリをダウンロードして、実行パスの通った ディレクト リに配置します。 opa eval コマンドでポリシーを評価します。 # 設問を満たしているか検証します。結果は "false" (満たしていない)でした。 $ opa eval -i input.json -d policy.rego --format pretty data.example.allow false # 設問のポリシーを満たしていないサーバは "ci","busybox" であることが確認できます。 $ opa eval -i input.json -d policy.rego --format pretty data.example.violation [ "ci", "busybox" ] または The Rego Playground からも実行できます。 Regoの文法 Regoの文法を簡単に説明します。 本章で扱う構造化データ 本章( Regoの文法 )では以下の構造化データをサンプルデータとして使用します。 { " persons ": [ { " name ": " alice ", " age ": 20 , " height ": 170 , " weight ": 60 } , { " name ": " bob ", " age ": 22 , " height ": 180 , " weight ": 80 } , { " name ": " carol ", " age ": 17 , " height ": 175 , " weight ": 66 } , { " name ": " dave ", " age ": 18 , " height ": 155 , " weight ": 50 } ] } 変数 変数の束縛 Regoの変数は「指定された条件を満たすよう束縛された値」です。 例えば以下の場合、変数 i は 配列 persons のうち「20才以下かつ身長170cm以上の人」を指す任意の添字 の値、つまり 0 と 2 に束縛されます。 some i persons[i].age <= 20 persons[i].height >= 170 手続き型 プログラミング言語 のように値を再び代入することはできません。 i := 1 i := 2 # i は既に 1 に束縛にされているためエラーになる また特殊な変数として _ があります。 _ は登場する度に他の変数と競合しない一意な変数へ変換されます。再び参照する必要のない変数は _ を使うことが推奨されています。 _ を使って先程の例を書き換えると以下のようになります。 person := persons[_] person.age <= 20 person.height >= 160 配列、集合、オブジェクトへのアクセス Regoには以下のような型があります。 数値 文字列 ブール値 配列 集合 オブジェクト(辞書) 配列 arr から要素 val を取りだす際は以下のように書きます。 # 変数を使った書き方。iは配列の添字、valは配列の値に束縛される。 some i val := arr[i] # または `_` を使った書き方。 val := arr[_] 集合 a_set から要素 val を取りだす際は以下のように書きます。 # valは集合の各要素に束縛される a_set[val] オブジェクト obj のキー key にアクセスする際は以下のように書きます。 # `.` を使った書き方 value := obj.key # または `[]` を使った書き方 value := obj["key"] ルール OPAでは、渡された構造化データ(ベースドキュメントとも言います)を元に、新しい構造化データ(仮想ドキュメントとも言います)を算出します。 この仮想ドキュメントの定義を ルール と言います。OPAでは様々なルールを定義することでポリシーを実装します。 集合を生成する 集合を生成するルールは以下のように記述します。 <name>[<value>] { <body> } このときルール <name> は「 <body> の式が全て真になるときの <value> 」を要素として持つ集合になります。 <body> の各式は 論理積 (AND)で結合されます。 以下は 「年齢20才以下、身長170cm以上の人」の名前の集合 を生成するルールの例です。 young_and_tall_persons[person.name] { person := input.persons[_] person.age <= 20 person.height >= 170 } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func youngAndTallPersons(input Input) [] string { result := [] string {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result = append (result, person.name) } } return result } オブジェクト(辞書)を生成する オブジェクトを生成するルールは以下のように記述します。 <name>[<key>] = <value> { <body> } このときルール <name> は「 <body> の式が全て真になるときの <key> と <value> のペア」を要素として持つオブジェクトになります。 <body> の各式は 論理積 (AND)で結合されます。 以下は 「年齢20才以下、身長170cm以上の人」について、名前と体重のペアを要素として持つのオブジェクト を生成するルールの例です。 young_and_tall_persons_weight[person.name] = person.weight { person := input.persons[_] person.age <= 20 person.height >= 170 } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func youngAndTallPersonsWeight(input Input) map [ string ] int { result := map [ string ] int {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result[person.name] = person.weight } } return result } 値を生成する 値を生成するルールは以下のように記述します。 <name> = <value> { <body> } このときルール <name> は「 <body> の式が全て真になるときの <value> の値」になります。 <body> の各式は 論理積 (AND)で結合されます。 以下は「Aliceの身長」を返すルールの例です。 alice_height = person.height { person := input.persons[_] person.name == "alice" } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func aliceHeight(input Input) int { result := 0 for _, person := range input.persons { if person.name == "alice" { result = person.height } } return result } <vaule> には数値、文字列、真偽値だけでなく、集合、配列、オブジェクトなども指定できます。 = <value> 部を省略すると = true として扱われます。 例えば以下のルール alice_exists は name キーの値が alice な要素が存在する場合のみ true になります。 alice_exists { person := input.persons[_] person.name == "alice" } { <body> } 部を省略すると { true } として扱われます。定数はこの書き方を使って定義します。 pi = 3.14 複数の定義を書く ルールは複数に分割して定義できます。 分割して記述されたルールは 論理和 (OR)のように評価されます。 以下は 「 BMI が18.5未満または25以上の人」の名前の集合 を返すルールの例です。 unhealthy_persons[person.name] { person := input.persons[_] bmi := (person.weight / (person.height * person.height)) * 10000 bmi < 18.5 } unhealthy_persons[person.name] { person := input.persons[_] bmi := (person.weight / (person.height * person.height)) * 10000 bmi >= 25 } これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 func unhealthyPersons(input Input) [] string { result := [] string {} for _, person := range input.persons { bmi := float64 (person.weight) / ( float64 (person.height) * float64 (person.height)) * 10000.0 if bmi < 18.5 || bmi >= 25 { result = append (result, person.name) } } return result } 関数 関数は以下のように記述します。引数を取ること以外は 値を生成するルール と概ね同じです。 <name> (<args>) = <value> { <body> } 以下は BMI を計算する関数と、 BMI の分類を返す関数の例です。 bmi(height, weight) = (weight / (height * height)) * 10000 bmi_class(person) = "underweight" { bmi(person.height, person.weight) < 18.5 } bmi_class(person) = "normal range" { bmi(person.height, person.weight) >= 18.5 bmi(person.height, person.weight) < 25 } bmi_class(person) = "overweight" { bmi(person.height, person.weight) >= 25 } またOPA/Regoには組込み関数があります。よく使う組込み関数をいくつか紹介します。 # 書式付き文字列を評価して結果の文字列を返す sprintf("%s's weight is %d", [person.name, person.weight]) # 配列、集合、オブジェクトの要素数を返す count(young_and_tall_persons) == 0 # オブジェクトを文字列(json形式)に変換したものを返す json.marshal(obj) # --explainオプションをつけて実行した際に文字列を出力する(詳細は後述) trace("...") 上で紹介した以外にも様々な組込み関数があります。詳細は Built-in Functions を参照してください。 内包表記 Regoでは内包表記を使用できます。 リスト内包表記 以下は 「年齢20才以下、身長170cm以上の人」の名前の配列 を生成する内包表記の例です。 array := [person.name | person := input.persons[_]; person.age <= 20; person.height >= 170] これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 array := func (input Input) [] string { result := [] string {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result = append (result, person.name) } } return result }(input) 集合内包表記 以下は 集合を生成する で例示した 「年齢20才以下、身長170cm以上の人」の名前の集合 を内包表記で記述した例です。 a_set := {person.name | person := input.persons[_]; person.age <= 20; person.height >= 170} これをGoで書いた場合のコードは リスト内包表記 と同じため割愛します(※あくまでイメージです)。 オブジェクト内包表記 以下は オブジェクト(辞書)を生成する で例示した 「年齢20才以下、身長170cm以上の人」について、名前と体重のペアを要素として持つのオブジェクト を内包表記で記述した例です。 obj := {person.name: person.weight | person := input.persons[_]; person.age <= 20; person.height >= 170} これをGoのコードで書くと以下のようになります(※あくまでイメージです)。 obj := func (input Input) map [ string ] int { result := map [ string ] int {} for _, person := range input.persons { if person.age <= 20 && person.height >= 170 { result[person.name] = person.weight } } return result }(input) キーワードと 演算子 not 式を否定するには not を使用します。 not person.age <= 20 ルールが仮想ドキュメントを生成したか 配列や集合やオブジェクトの中に指定した要素が存在するか を判定する際にも使用します。 not input.persons[4] not person.birthday some some を使うことでルール内のローカル変数を明示的に宣言できます。 young_and_tall_persons[person.name] { some i person := input.persons[i] person.age <= 20 person.height >= 170 } ローカル変数の宣言を必ず行う必要はありませんが、変数名と同じルールが存在した場合でも期待通りローカル変数として扱われるよう、明示的に宣言することが推奨されています。 例えば以下のルール young_and_tall_persons は some i の有無で結果が変わります。 i = 1 young_and_tall_persons[person.name] { some i person := input.persons[i] person.age <= 20 person.height >= 170 } default default を使うことで 値を生成するルール のデフォルト値を指定できます。 以下の例では、ルール alice_exists は input.persons の中にAliceの要素が存在すれば true 、そうでなければ false になります。 default キーワードを省略すると、ルール alice_exists は input.persons の中にAliceの要素が存在しない場合 false ではなく未定義になります。 default alice_exists = false alice_exists { person := input.persons[_] person.name == "alice" } in in はOPAの v0.34.0 で導入されたキーワードです。 配列、集合、オブジェクトがある値を要素に含んでいるか(または含んでいないか)判定する 配列、集合、オブジェクトの要素に変数を束縛する といったことができます。 OPA v0.34.2 時点では、 in キーワードを使うには import future.keywords.in を宣言する必要があります。 以下は配列、集合、オブジェクトがある値を要素に含んでいるか判定するルールの例です。 import future.keywords.in # 名前の集合。結果は ["alice", "bob", "carol", "dave"] 。 person_names[person.name] { person := input.persons[_] } # 名前と体重のペアからなるオブジェクト。結果は {"alice": 60, "bob": 80, "carol": 66, "dave": 50} 。 person_weights[person.name] = person.weight { person := input.persons[_] } # 集合 person_names の中に "alice" が存在するか。結果は true 。 alice_exists { "alice" in person_names } # オブジェクト person_weights の中に値が 60 である要素(例: {"alice": 60} )が存在するか。結果は true 。 sixty_kg_person_exists { 60 in person_weights } not と組み合わせて、配列、集合、オブジェクトがある値を要素に含んでいないかを判定できます。 # 集合 person_names の中に "ellen" が存在しないか。結果は true 。 ellen_does_not_exist { not "ellen" in person_names } # オブジェクト person_weights の中に値が 70 である要素(例: {"alice": 70} )が存在しないか。結果は true 。 seventy_kg_person_does_not_exist { not 70 in person_weights } 左辺に引数を2つ渡すことで、配列、オブジェクトが「添字、キー」と「値」のペアを含んでいるかを判定できます。 # オブジェクト person_weights の中に要素 {"alice": 60} が存在するか。結果は true 。 is_alice_sixty_kg { "alice", 60 in person_weights } 配列、集合の定義 関数の引数 などで in キーワードの左辺に引数を2つ渡す場合は、正しく評価されるよう () で囲む必要があります。 # 誤った書き方。「`0`と `2 in [2]` の2つの引数を受け取る関数f」として解釈される。 f(0, 2 in [2]) # 正しい書き方。「`0, 2 in [2]` の結果を1つの引数として受け取る関数f」として解釈される。 f((0, 2 in [2])) some と組み合わせて、配列、集合、オブジェクトの要素に変数を束縛できます。 # 体重が60kgの人の名前の集合を返す。結果は {"alice"} 。 sixty_kg_persons[name] { some name, 60 in person_weights } # 結果は ["a", "r", "y"] unique[x] { some x in ["a", "r", "r", "a", "y"] } with ルールの評価時に with を使って input data.<path> の構造化データを指定した値に置き換えることができます。 allow with input as {"user": "charlie", "method": "GET"} with data.roles as {"dev": ["charlie"]} これは主に 単体テスト を書く際に使用します。テストの書き方は テストを書く で説明します。 モジュール、パッケージ 0個以上のルールの集合をモジュールといい、それを特定の 名前空間 にグループ化したものをパッケージといいます。 同じパッケージのポリシーを同じ ディレクト リに配置する必要はありません。 パッケージ名は package で指定します。 package example 他のパッケージのルールや関数を参照する場合は import data.<パッケージ名> のように宣言します。 import data.other_example example_rule[value] { value := other_example.other_rule } テストを書く 単体テスト を書くことでポリシーが期待通り動作するか確認できます。 テストコードはファイル名の末尾を _test.rego としてください。 例えば example.rego のテストコードのファイル名は example_test.rego です。 テストケースは名前が test_ から始まるルールとして記述します。ルールのbody部の式が全て真ならば成功、そうでなければ失敗です。 package example test_young_and_tall_persons { input := {"persons": [ {"name": "alice", "age": 20, "height": 170, "weight": 60}, {"name": "bob", "age": 22, "height": 180, "weight": 80}, {"name": "carol", "age": 17, "height": 175, "weight": 66}, {"name": "dave", "age": 18, "height": 155, "weight": 50}, ]} young_and_tall_persons == {"alice", "carol"} with input as input } test_no_young_and_tall_persons { input := {"persons": [ {"name": "alice", "age": 30, "height": 170, "weight": 60}, {"name": "bob", "age": 22, "height": 180, "weight": 80}, {"name": "carol", "age": 27, "height": 175, "weight": 66}, {"name": "dave", "age": 18, "height": 155, "weight": 50}, ]} count(young_and_tall_persons) == 0 with input as input } テストは以下のように実行します。 $ opa test . PASS: 2/2 試しにテストケース test_young_and_tall_persons の アサーション の右辺を {"alice", "carol"} から {"alice", "bob"} へ書き換えてテストを失敗させます。 テストが失敗した場合は次のような結果になります。 $ opa test . data.example.test_young_and_tall_persons: FAIL (1.466812ms) -------------------------------------------------------------------------------- PASS: 1/2 FAIL: 1/2 opa test コマンドには以下のようなオプションもあります。 -v, --verbose :テスト結果の詳細を表示します。 -r, --run :指定したテストケースのみを実行できます。 -c, --coverate :テスト カバレッジ を出力できます。 OPA/Regoを利用するツール OPA/Regoを利用するツールを簡単にご紹介します。 Conftest Conftest は Kubernetes の マニフェスト ファイルやTerraformコードなどの構成ファイルがRegoで記述されたポリシーに従っているか検証するための CLI ツールです。 以下は Kubernetes のPodに Readiness Probe が設定されているか検証するポリシーの例です(※このポリシーは説明のために簡略化しており実運用には適していません)。 package deny_container_without_readiness_probe violation[msg] { input.kind == "Pod" container := input.spec.containers[_] not container.readinessProbe msg := "Readiness Probe must be set" } 先程のポリシーを用いて以下の マニフェスト ファイルを検証してみます。 apiVersion : v1 kind : Pod metadata : name : myapp spec : containers : - name : myapp image : myapp:1.0.0 ports : - containerPort : 8080 $ conftest test --policy . --all-namespaces manifests.yaml FAIL - manifests.yaml - deny_container_without_readiness_probe - Readiness Probe must be set 1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions マニフェスト がポリシーに違反していることを検出できました。 Conftestは Kubernetes の マニフェスト ファイル以外の構成ファイルに対しても利用できます。公式ドキュメントの Examples には以下の構成ファイルを検証するサンプルポリシーが掲載されています。 AWS SAM Framework CUE Docker compose Dockerfile EDN Ignore HCL HCL 2 HOCON INI Jsonnet Kubernetes Kustomize Serverless Framework Traefik Typescript VCL XML Gatekeeper Gatekeeper は Kubernetes の Validating Admission Webhook として動作します。 kube-apiserverに対するリソース作成の要求を検証し、ポリシーに違反するリソースの作成を禁止できます。 Kubernetes におけるConftestと Gatekeeper の使い分け Conftestは主に開発者のローカル環境やCIサーバなどで実行されます。 git リポジトリ 上の マニフェスト ファイルが変更された際に、その変更がポリシーに従っているか検証し、即座に開発者へフィードバックできます。 ただし以下のようなケースはConftestで検知できません。 オペレーターが kubectl create などで Kubernetes クラスタ ーにリソースを直接作成した場合 Kubernetes クラスタ ー上のCustomControllerによってリソースが作成された場合 Gatekeeper は Kubernetes のValidating Admission Webhookとして動作します。 kube-apiserverに対する全てのリソース作成の要求を検証し、ポリシーに違反するリソースの作成を禁止できます。 ただし kubectl create や kubectl apply を実行して実際にkube-apiserverへリク エス トを送信するまで、ポリシーに違反しているか検証できません。 Conftestと Gatekeeper はどちらか片方だけ使うのではなく、両方組み合わせて使うのがよいでしょう。 主な実行場所 主な実行タイミング 検証対象 Conftest 開発者のローカル環境、CIサーバ マニフェスト ファイルの更新時 マニフェスト ファイル Gatekeeper Kubernetes のValidating Admission Webhook kube-apiserverへのリソース作成の要求時 kube-apiserverに対する全てのリソース作成の要求 Conftestも Gatekeeper もRegoで記述されたポリシーを使用します。 しかし構造化データ(= マニフェスト )の渡し方などに差異があります。 Conftestと Gatekeeper で同じポリシーを使用する場合、何らかの方法でこの差分を吸収する必要があります。 マニフェスト の格納場所 Conftest( --combine オプションなし)(*1) input Conftest( --combine オプションあり)(*1) input[_].contents Gatekeeper input.review.object (*1):Conftestは通常 マニフェスト ファイルに含まれるリソースを1つずつ個別に評価します。 そのため、例えばDeploymentに対してPodDisruptionBudgetが定義されているかといったような、複数のリソースにまたがったポリシーを評価できません。 --combine オプションを設定することで、 マニフェスト ファイルに含まれる全てのリソースを同時にRegoへ渡し、複数のリソースにまたがったポリシーを評価できます。 またConftestが使用する *.rego ファイルから Gatekeeper が必要とするリソース( ConstraintTemplate など)を生成する必要があります。 これを行うツールに konstraint があります。 それ以外のツール これまで紹介したツールの他に、以下のツールもRegoで記述されたポリシーを利用しています。 Trivy Terrascan ハマったところ 私が実際にOPA/Regoを使っていてハマった点をいくつかご紹介します。 矛盾したルールを定義しない ルールの定義が矛盾してはいけません。そのようなルールに矛盾を発生させる構造化データを渡すと実行時エラーが発生します。 いくつか悪い例を紹介します。 以下は値を生成するルールです。ただし値が1つに定まりません。 height = person.height { person = input.persons[_] } 以下の例ではルールの定義が複数に分かれています。 1つ目の定義では「 name と weight のペアを要素として持つオブジェクト」を生成します。 一方、2つ目の定義では「 name と height のペアを要素として持つオブジェクト」を生成します。 結果、生成されたオブジェクトの name キーの値は1つに定まりません。 personal_health_info[person.name] = person.height { person := input.persons[_] } personal_health_info[person.name] = person.weight { person := input.persons[_] } 配列や集合がある条件を満たす値を含んでいないか検証するルールの書き方 配列 array や集合 a_set が関数 f を満たす値を含まないか判定するルールは以下のように記述できます。 # 配列が関数fを満たす値を含んでいないか none_in_array_match { count({x | x := array[_]; f(x)}) == 0 } # 集合が関数fを満たす値を含んでいないか none_in_set_match { count({x | a_set[x]; f(x)}) == 0 } 特に配列 array や集合 a_set がある値 x を要素に含んでいないか判定したい場合は in を使って以下のように記述できます。 ※OPA v0.34.2 時点では、 in キーワードを使うには import future.keywords.in を宣言する必要があります。 import future.keywords.in # 配列がある値を含んでいないか none_in_array_match { not x in array } # 集合がある値を含んでいないか none_in_set_match { not x in a_set } 以下の書き方は誤りです。 以下のルールは配列や集合に関数 f を満たさない値が1つでも存在すれば true になります。 配列や集合の全ての要素が関数 f を満たさないことは検証できません。 # 配列が関数fを満たす値を含んでいないか(間違った書き方) none_in_array_match { x := array[_] not f(x) } # 集合が関数fを満たす値を含んでいないか(間違った書き方) none_in_set_match { a_set[x] not f(x) } オブジェクトのフィールドが特定の値であるか検証するルールの書き方 オブジェクト obj のキー key の値が value であることを判定するルールは以下のように記述できます。 violation[msg] { not has_specific_value(obj) msg := "'obj.key' must be 'value'" } has_specific_value(obj) { obj.key == value } 以下の書き方は誤りです。 「オブジェクト obj のキー key の値が value である場合」に加えて「オブジェクト obj にキー key が存在しない場合」にもルールのbody部が false になるためです。 violation[msg] { obj.key != value msg := "'obj.key' must be 'value'" } 異常系テストの アサーション の書き方 以下は入力データの値が偶数かどうか判定するポリシーの例です。 violation[msg] { input.value % 2 != 0 msg := "Value must be even" } 上述のポリシーに対する異常系テストとして、最初私は以下のようなテストケースを記述していました。 test_odd { input := {"value": 1} violation with input as input } ですが上述のテストケースは下表の通り常に成功してしまいます。 これは {} ( 空集合 )が真として扱われるためです。 よってこのテストケースの書き方は誤りです。 value の値 violation の結果 test_odd の結果 奇数(例:1, 3, ...) {"Value must be even"} true 偶数(例:2, 4, ...) {} ( 空集合 ) true 参考: open policy agent - Rego testing: how to test "not deny"? - Stack Overflow 正しくは以下のいずれかのように記述するとよいでしょう。上の方がより厳密に検証を行います。 # 例1:violationが指定した値に完全に一致するか確認する。 test_odd { input := {"value": 1} violation == {"Value must be even"} with input as input } # 例2:violationに指定した要素が含まれているか確認する。他の要素が含まれている可能性もある。 test_odd { input := {"value": 1} violation["Value must be even"] with input as input } # 例3:violationが空でないことを確認する。具体的な要素までは確認しない。 test_odd { input := {"value": 1} count(violation) > 0 with input as input } オブジェクトの存在しないキーを参照した際の挙動 オブジェクトの存在しないキーを参照するとルールが意図しない結果を返すため注意が必要です。 先程の偶数かどうか判定するポリシーを誤って以下のように記述したとします。 violation[msg] { input.number % 2 != 0 # 値が格納されているのは input.value だが誤って input.number を参照している msg := "Value must be even" } このとき input.value がどのような値でも violation は 空集合 を返します。 これはルール violation のbody部における1番目の式が、 input.value の値にかかわらず、常に未定義(つまり真ではない)になるためです。 $ cat input_odd.json { "value": 1 } $ opa eval -i input_odd.json -d policy.rego --format pretty data.example.violation [] $ cat input_even.json { "value": 2 } $ opa eval -i input_even.json -d policy.rego --format pretty data.example.violation [] ここで問題なのは未定義が false と同様に扱われていることです。 ルールの中でオブジェクトの存在しないキーを参照して未定義が発生しても特に例外などは発生しません。 そのため正しくポリシーが評価されてルールのbody部が false になったのか、オブジェクトの存在しないキーを参照して未定義になったのか、区別するのは困難です。 現時点では、 単体テスト の カバレッジ を上げる以外に、この問題を解決するよい方法は見つけられていません。 またこれは「OPAのポリシーエンジン」と「それを呼びだす外部サービス」のインターフェース部分の仕様が変更された場合も問題になります。 例えばConftestでは v0.22.0 で --combine オプションをつけた際にRegoへ渡すデータの構造が変更されました( #388 )。 結果、いくつかのルールがオブジェクトの存在しないキーを参照して必ず未定義になり、 マニフェスト がポリシーに違反していてもConftestの検証が常に成功し続けるという問題が起こりました。 これを防ぐ方法として、Regoの 単体テスト に加えて「OPAのポリシーエンジン」と「それを呼びだす外部サービス」の間の 結合テスト を実装することが挙げられます。 例えば私はConftestのテスト結果を conftest test -o json で出力しておき、Conftestをバージョンアップする際はテスト結果に意図しない変更が発生していないか確認するスナップショットテストを行うようにしています。 ポリシーの デバッグ 方法 ポリシーの結果が期待通りでない場合、原因を調査する必要があります。ですがRegoの処理の流れは複雑で追うのが困難です。 ポリシーの挙動を把握する方法として --explain オプションを活用できます。 例として以下のポリシーと構造化データを --explain オプションをつけて評価します。 violation[msg] { trace(sprintf("input.value is %d", [input.value])) input.value % 2 != 0 msg := "Value must be even" } { " value ": 2 } --explain オプションの値とそれに対する出力は以下のとおりです。 --explain=full :ルールがどのように評価されたか全て表示する。 $ opa eval -i input.json -d policy.rego --explain full --format pretty data.example.violation query:1 Enter data.example.violation = _ query:1 | Eval data.example.violation = _ query:1 | Index data.example.violation (matched 1 rule) policy.rego:3 | Enter data.example.violation policy.rego:4 | | Eval __local3__ = input.value policy.rego:4 | | Eval sprintf("input.value is %d", [__local3__], __local1__) policy.rego:4 | | Eval trace(__local1__) policy.rego:4 | | Note "input.value is 2" policy.rego:5 | | Eval __local4__ = input.value policy.rego:5 | | Eval rem(__local4__, 2, __local2__) policy.rego:5 | | Eval neq(__local2__, 0) policy.rego:5 | | Fail neq(__local2__, 0) policy.rego:5 | | Redo rem(__local4__, 2, __local2__) policy.rego:5 | | Redo __local4__ = input.value policy.rego:4 | | Redo trace(__local1__) policy.rego:4 | | Redo sprintf("input.value is %d", [__local3__], __local1__) policy.rego:4 | | Redo __local3__ = input.value query:1 | Exit data.example.violation = _ query:1 Redo data.example.violation = _ query:1 | Redo data.example.violation = _ [] --explain=fails :式が false になった部分のみ表示する。 $ opa eval -i input.json -d policy.rego --explain fails --format pretty data.example.violation query:1 Enter data.example.violation = _ policy.rego:3 | Enter data.example.violation policy.rego:5 | | Fail neq(__local2__, 0) [] --explain=notes : trace 関数(後述)の内容のみ表示する。 $ opa eval -i input.json -d policy.rego --explain notes --format pretty data.example.violation query:1 Enter data.example.violation = _ policy.rego:3 | Enter data.example.violation policy.rego:4 | | Note "input.value is 2" [] trace 関数を使うと --explain オプションをつけて実行した際に文字列を出力できます。 trace 関数の引数は文字列ですが sprintf 関数や json.marshal 関数と組み合わせることで文字列以外の値(数値やオブジェクトなど)も出力できます。 これを使ってprint デバッグ できます。 --explain=full の出力を読み解くのはなかなか大変なので、最初は trace 関数と --explain=notes オプションを組み合わせてprint デバッグ するのがよいでしょう。 Conftestでも --trace オプションをつけることでルールがどのように評価されたか表示できます。これは opa コマンドの --explain=full に相当します。 = と := と == の違い Regoには = 、 := 、 == という似たような 演算子 があります。それぞれの違いは以下のとおりです。 記法 記述できる場所 コンパイル エラー ユースケース := ルール内 変数が既に定義されている場合に発生 ローカル変数を定義する == ルール内 変数がまだ定義されていない場合に発生 値を比較する = どこでも 変数が正しく参照できない場合に発生 クエリを表現する := と == のかわりに = を使うことができます。 ただし := と == は コンパイル 時に追加のチェックが働くため、使える場面ではなるべく = よりも := と == を使うとよいでしょう。 参考: Equality: Assignment, Comparison, and Unification フォーマッタを使う opa fmt --write . で現在の ディレクト リ配下にあるポリシーのフォーマットを整えることができます。 複数人でポリシーを更新する際はこのコマンドを使ってインデントや改行の有無などを統一するとよいでしょう。 また opa fmt --fail . でポリシーのフォーマットが整っていなかった場合に0以外の終了コードを返すことができます。 CIに組み込んでポリシーのフォーマットが整っているか検証するとよいでしょう。 おわりに このポストではOpen Policy Agentとポリシー言語Regoの紹介をしました。 前半ではRegoの文法を簡単に説明しました。 後半では私がOpen Policy AgentとRegoを実際に使っていてハマった点をいくつかご紹介しました。 このポストが読んでくださった方の役に立てば幸いです。 さて、明日は橋詰さんの「今年社内の技術書読書会で読んだ本!」です。 どんな技術書が紹介されるのか楽しみです! 最後までお読みいただきありがとうございました。 参考 Open Policy Agent | Documentation 特に Policy Reference はRegoの色々な書き方が紹介されていて参考になります。 Open Policy Agent Rego Knowledge Sharing Meetup - connpass Achieving Security Compliance Monitoring with Open Policy Agent and Rego - Speaker Deck 【開催報告】新聞社による AWS を活用した DX 事例セミナー | Amazon Web Services ブログ Gatekeeper/conftestのRegoをDRYに管理する — HACK The Nikkei 執筆: @shibata.takao 、レビュー: @ueba.yuki ( Shodo で執筆されました )
アバター
はじめまして、ISID エンタープライズ xRセンター Lumiscaphe担当の加納です。 ISIDアドベントカレンダー も4日目ですね! 今回はLumiscaphe系ソフトを用いたワークフローの中核をなす、Patchwork3D…通称「P3D」の紹介をしたいと思います。 Lumiscaphe担当の私はもともと、 プロダクトデザイナー として3D系のソフトを使用していました。それを踏まえて、実際に使用する中でどのように役立ちそうか?どこが良いのか?等をお伝えします。 リアルタイムレンダラーとは? P3Dの特徴 UV展開が得意 布表現が得意 塗装表現が得意 誤操作を起こしにくい データの活用 AccelVR Lumis3D 最後に リアルタイムレンダラーとは? レンダラーは聞いたことがあるけど、「リアルタイムレンダラー」は耳慣れない。という方もいらっしゃるのではないでしょうか? 3Dデータに質感を付け写実的な形に整えるのがレンダラーの役割ですが、そのレンダラーにもいくつか種類があります。 1つは建築などで一般的に用いられる レイトレースレンダラー 。光の反射回数などを指定し、空気感や複雑な反射に長けたタイプのレンダラーです。 特徴は光の反射を追って画面を作るため、じっくり時間をかけてフォトリアルな画面を作っていくことにあります。反面、きちっと1枚の画を作るのに時間がかかると言い換えることも出来ます。上のGIF動画で見比べると分かりやすいですね。 もう一つが リアルタイムレンダラー です。こちらは文字通りリアルタイム性を重視しており、フォトリアルさを追求しようとするとレイトレース以上に技量を求められます。しかしその分リアルタイムで反射を演算できるため、反射を見て面の繋がりを確認する…といったシビアな評価にも使うことが出来ます。 リアルタイムレンダラーは VR 表示可能なソフトが多く、このP3DもAccelVRというソフトを用いた VR 表示が可能です。 製造業デザインにおけるデジタル モックアップ として使用できるタイプのレンダラー と言えるでしょう。 なお最近はレイトレース系としてリアルタイムレイトレースレンダラーなんてものもありますが、反射を見て面の繋がりを確認する用途には少し厳しいだろうというのが個人的な感想です。それぞれ違う用途があるというわけですね。 P3Dの特徴 UV展開が得意 例えばチェック柄のついた布にクシャッとしわを寄せたとき、チェック柄はしわに沿って歪みますよね。3D上でそれを再現するために必要な処理が「UV展開」なのですが、この処理が実はとても厄介です。 それを簡単に行えるのがP3Dの強みです。 布表現が得意 布のように柔らかく形が変わりやすいもののUV展開を容易に行える上、布のマテリアル表現が得意です。 薄い層を重ねるようにして効果を重ねてマテリアルを作る「マルチレイヤーマテリアル」が多彩な表現を可能にし、近付いてもリアルな表現を維持します。 VR でこの質感を体感できると思うとワクワクしませんか? 塗装表現が得意 実際に塗装をする際、吹き付け1回で塗装完了!ということはありませんよね。 通常は、色のついた塗料を吹き付けた後で重ねてクリアコートを吹き付ける…といったように、2工程以上に分かれているものではないでしょうか。P3Dでは前述の「マルチレイヤーマテリアル」が重ね表現を可能にし、現実と同様の工程を踏んでの塗装表現を可能にしています。 レイヤーなしで同様の表現をすることももちろんできるのでしょうが、レイヤーを用いることでより編集が簡単になり、ひいては作成時間の短縮にも繋がります。 青っぽい要素を足したいけど、足す前との比較をしながら検討したい!といった場合にも、マテリアル自体を増やさずにレイヤーのON・OFFで比較出来ますしね。 誤操作を起こしにくい 他のレンダラーを触っているときに、テクスチャを掴んで動かしたいのに、うっかり サーフェス を触ってしまった… サーフェス が動かないな?と思いながら触っていたらテクスチャを動かしており、Ctrl+Zで戻せる回数を超えてしまった… なんてこと、ありませんか?恥ずかしながら私はまあまあ心当たりがあるのですが、P3Dでは起きません。 なぜならP3Dでは、形状・ サーフェス のレイヤー構造を編集する「Shaper」画面と、テクスチャなど見た目を編集する「Matter」画面に分かれているからなのです。 画面の作りに関して個人的な意見を付け加えさせていただくと、 Adobe ライクなUIで、特にデザイナーの方にやさしい構造だと感じています。多機能でありつつも、3Dを全く触ったことがなくても使える作りのレンダラーだと言えるでしょう。 データの活用 冒頭で「Lumiscaphe系ソフトを用いたワークフロー」と書いた通り、P3Dはデータの活用先が多彩です。 VR 表示で見られるほか、 GPU を積まないPCや タブレット 、 スマートフォン などで レンダリング を表示できます。簡単にご紹介いたしますね。 AccelVR レンダラーの種類紹介の部分で少し触れたソフトです。 VR 表示を可能にする関連ソフトで、P3Dと変わらない品質での VR 表現ができます。P3Dから専用形式に書き出して読み込むことで、 HMD (Head Mounted Display…つまり頭に被るタイプの VR )のほか、スクリーンに囲まれた部屋へ人間が入るタイプの VR 「CAVE」などでも表示可能です。 専用形式に書き出して別データで持つ必要があるというのは面倒に思われがちですが、実は管理の上でメリットがあります。 1つは書き出した時点での見え方をキープできるということ。 ちょっと実体験が混ざってしまうのですが、編集中のデータを途中報告した際に「〇日に見たデータってどんなんだっけ?」と言われることがあるんですよね。編集中データだから、もう更新してしまっているのに…。 そんな時に、別ファイル書き出しのワークフローが挟まっていると救われることがあります。 もう1つは、データの独り歩きを防ぐことが出来ます。 AccelVRのファイル形式はAccelVRと後述のLumis3Dでしか使えない上、原則として編集可能データへの戻しが不可能です。CADオプションを入れていればAccelVR内で描画した形状のみCADへの戻しが可能ですが、データの流出防止に便利なのは間違いありません。 Lumis3D 「サーバーサイド レンダリング 」という概念をご存じでしょうか。 通常、 レンダリング というとPCの処理能力が高くないとできないし、開始すると冷却ファンがうなりだすものだ…というイメージはありませんか? サーバーサイド レンダリング では、 レンダリング 処理を遠隔地に置いたサーバーで行います。処理速度はサーバーのスペックに依存しますが、重たい処理をPC内で行わないため、 GPU を搭載しないPCでも問題なく表示できます。もちろん タブレット での表示も可能です。 用途としては、一般PCでのデザイン確認(検討や承認過程など)・販 売店 でのデザインバリエーション提示・ ECサイト へ組み込んでユーザー自身にデザインバリエーションを確認してもらう、などが想定されています。Lumis3D内でP3DファイルやPDFを開くことができるので、デザイン確認の可能なオンラインデータ共有ツールとしても使用できます。 最後に いかがでしょうか。LumiscapheやP3D、興味を持っていただけましたか? ISIDにて私の属するExRCでは、様々なソフトやサービス・最新機器のご紹介を行っております。まだまだ新しい組織でコンテンツも増やしているところですが、P3Dの詳しい使い方講座なども行っておりますのでぜひ一度覗きに来てみてください。 最後までお読みいただきありがとうございました。 まだまだ続く アドベントカレンダー 、5日の更新をお楽しみに! 執筆: @kano.nanami 、レビュー: @higa ( Shodo で執筆されました )
アバター
はじめまして、ISID エンタープライズ xRセンター Lumiscaphe担当の加納です。 ISIDアドベントカレンダー も4日目ですね! 今回はLumiscaphe系ソフトを用いたワークフローの中核をなす、Patchwork3D…通称「P3D」の紹介をしたいと思います。 Lumiscaphe担当の私はもともと、 プロダクトデザイナー として3D系のソフトを使用していました。それを踏まえて、実際に使用する中でどのように役立ちそうか?どこが良いのか?等をお伝えします。 リアルタイムレンダラーとは? P3Dの特徴 UV展開が得意 布表現が得意 塗装表現が得意 誤操作を起こしにくい データの活用 AccelVR Lumis3D 最後に リアルタイムレンダラーとは? レンダラーは聞いたことがあるけど、「リアルタイムレンダラー」は耳慣れない。という方もいらっしゃるのではないでしょうか? 3Dデータに質感を付け写実的な形に整えるのがレンダラーの役割ですが、そのレンダラーにもいくつか種類があります。 1つは建築などで一般的に用いられる レイトレースレンダラー 。光の反射回数などを指定し、空気感や複雑な反射に長けたタイプのレンダラーです。 特徴は光の反射を追って画面を作るため、じっくり時間をかけてフォトリアルな画面を作っていくことにあります。反面、きちっと1枚の画を作るのに時間がかかると言い換えることも出来ます。上のGIF動画で見比べると分かりやすいですね。 もう一つが リアルタイムレンダラー です。こちらは文字通りリアルタイム性を重視しており、フォトリアルさを追求しようとするとレイトレース以上に技量を求められます。しかしその分リアルタイムで反射を演算できるため、反射を見て面の繋がりを確認する…といったシビアな評価にも使うことが出来ます。 リアルタイムレンダラーは VR 表示可能なソフトが多く、このP3DもAccelVRというソフトを用いた VR 表示が可能です。 製造業デザインにおけるデジタル モックアップ として使用できるタイプのレンダラー と言えるでしょう。 なお最近はレイトレース系としてリアルタイムレイトレースレンダラーなんてものもありますが、反射を見て面の繋がりを確認する用途には少し厳しいだろうというのが個人的な感想です。それぞれ違う用途があるというわけですね。 P3Dの特徴 UV展開が得意 例えばチェック柄のついた布にクシャッとしわを寄せたとき、チェック柄はしわに沿って歪みますよね。3D上でそれを再現するために必要な処理が「UV展開」なのですが、この処理が実はとても厄介です。 それを簡単に行えるのがP3Dの強みです。 布表現が得意 布のように柔らかく形が変わりやすいもののUV展開を容易に行える上、布のマテリアル表現が得意です。 薄い層を重ねるようにして効果を重ねてマテリアルを作る「マルチレイヤーマテリアル」が多彩な表現を可能にし、近付いてもリアルな表現を維持します。 VR でこの質感を体感できると思うとワクワクしませんか? 塗装表現が得意 実際に塗装をする際、吹き付け1回で塗装完了!ということはありませんよね。 通常は、色のついた塗料を吹き付けた後で重ねてクリアコートを吹き付ける…といったように、2工程以上に分かれているものではないでしょうか。P3Dでは前述の「マルチレイヤーマテリアル」が重ね表現を可能にし、現実と同様の工程を踏んでの塗装表現を可能にしています。 レイヤーなしで同様の表現をすることももちろんできるのでしょうが、レイヤーを用いることでより編集が簡単になり、ひいては作成時間の短縮にも繋がります。 青っぽい要素を足したいけど、足す前との比較をしながら検討したい!といった場合にも、マテリアル自体を増やさずにレイヤーのON・OFFで比較出来ますしね。 誤操作を起こしにくい 他のレンダラーを触っているときに、テクスチャを掴んで動かしたいのに、うっかり サーフェス を触ってしまった… サーフェス が動かないな?と思いながら触っていたらテクスチャを動かしており、Ctrl+Zで戻せる回数を超えてしまった… なんてこと、ありませんか?恥ずかしながら私はまあまあ心当たりがあるのですが、P3Dでは起きません。 なぜならP3Dでは、形状・ サーフェス のレイヤー構造を編集する「Shaper」画面と、テクスチャなど見た目を編集する「Matter」画面に分かれているからなのです。 画面の作りに関して個人的な意見を付け加えさせていただくと、 Adobe ライクなUIで、特にデザイナーの方にやさしい構造だと感じています。多機能でありつつも、3Dを全く触ったことがなくても使える作りのレンダラーだと言えるでしょう。 データの活用 冒頭で「Lumiscaphe系ソフトを用いたワークフロー」と書いた通り、P3Dはデータの活用先が多彩です。 VR 表示で見られるほか、 GPU を積まないPCや タブレット 、 スマートフォン などで レンダリング を表示できます。簡単にご紹介いたしますね。 AccelVR レンダラーの種類紹介の部分で少し触れたソフトです。 VR 表示を可能にする関連ソフトで、P3Dと変わらない品質での VR 表現ができます。P3Dから専用形式に書き出して読み込むことで、 HMD (Head Mounted Display…つまり頭に被るタイプの VR )のほか、スクリーンに囲まれた部屋へ人間が入るタイプの VR 「CAVE」などでも表示可能です。 専用形式に書き出して別データで持つ必要があるというのは面倒に思われがちですが、実は管理の上でメリットがあります。 1つは書き出した時点での見え方をキープできるということ。 ちょっと実体験が混ざってしまうのですが、編集中のデータを途中報告した際に「〇日に見たデータってどんなんだっけ?」と言われることがあるんですよね。編集中データだから、もう更新してしまっているのに…。 そんな時に、別ファイル書き出しのワークフローが挟まっていると救われることがあります。 もう1つは、データの独り歩きを防ぐことが出来ます。 AccelVRのファイル形式はAccelVRと後述のLumis3Dでしか使えない上、原則として編集可能データへの戻しが不可能です。CADオプションを入れていればAccelVR内で描画した形状のみCADへの戻しが可能ですが、データの流出防止に便利なのは間違いありません。 Lumis3D 「サーバーサイド レンダリング 」という概念をご存じでしょうか。 通常、 レンダリング というとPCの処理能力が高くないとできないし、開始すると冷却ファンがうなりだすものだ…というイメージはありませんか? サーバーサイド レンダリング では、 レンダリング 処理を遠隔地に置いたサーバーで行います。処理速度はサーバーのスペックに依存しますが、重たい処理をPC内で行わないため、 GPU を搭載しないPCでも問題なく表示できます。もちろん タブレット での表示も可能です。 用途としては、一般PCでのデザイン確認(検討や承認過程など)・販 売店 でのデザインバリエーション提示・ ECサイト へ組み込んでユーザー自身にデザインバリエーションを確認してもらう、などが想定されています。Lumis3D内でP3DファイルやPDFを開くことができるので、デザイン確認の可能なオンラインデータ共有ツールとしても使用できます。 最後に いかがでしょうか。LumiscapheやP3D、興味を持っていただけましたか? ISIDにて私の属するExRCでは、様々なソフトやサービス・最新機器のご紹介を行っております。まだまだ新しい組織でコンテンツも増やしているところですが、P3Dの詳しい使い方講座なども行っておりますのでぜひ一度覗きに来てみてください。 最後までお読みいただきありがとうございました。 まだまだ続く アドベントカレンダー 、5日の更新をお楽しみに! 執筆: @kano.nanami 、レビュー: @higa ( Shodo で執筆されました )
アバター
これは 電通国際情報サービス アドベントカレンダー の3日目の記事です。 こんにちは。 電通国際情報サービス (ISID) イノベーション ラボの比嘉です。 うちの会社が、テックブログを始めるということなので、僕もブログを再開します。以前は個人のブログだったので、技術以外のエントリもいろいろありましたが、今後は会社のテックブログとしてやっていきます。 ブログでは基本的にEthereumのプログラミングについて書いていく予定です。僕自身、Ethereumについてほとんど知らないので、学んでいく過程をブログに書いていきます。また、できるだけ意図的にエラーを起こし、それを考察していこうと思います。失敗から学べることは多いからね。 Gethとは Gethのインストール MacへのGethのインストール WindowsへのGethのインストール プライベートネットワークへの接続 データディレクトリの作成 Genesisブロックの作成 Gethの起動 Geth attachサブコマンド etherの採掘 etherの送金 Gethとは Ethereumを利用する場合、まずはEthereumのネットワークに参加する必要があります。ネットワークへの参加には、Ethereumクライアントを使います。 Ethereumクライアントの中で公式に推奨されているクライアントがGethです。Gethは プログラミング言語 Goにより実装された CUI クライアントです。Gethを使うことで次のようなことができます。 etherの採掘 etherの送金 スマート・ コントラ クトの生成 Gethのインストール Gethのインストールの公式サイトはこちら Mac と Windows については、載せておきます。 Mac しか試していません。ごめんなさい。 Mac へのGethのインストール Homebrewを使うのが簡単です。Homebrewをインストールしてから次のコマンドを実行してください。 $ brew tap ethereum/ethereum $ brew install ethereum Windows へのGethのインストール インストーラ ーをダウンロードして実行しましょう。 ダウンロードサイトはこちら プライベートネットワークへの接続 Ethereum 本番環境ネットワークにいきなり接続するのは怖いですよね。まだ、Ethereumのこともよく知らないし。まずは、自分だけの閉じたネットワーク(プライベートネットワーク)を作成してそこに接続しましょう。 データ ディレクト リの作成 データを格納する ディレクト リを作って、そこに移動します。例として、private_netという ディレクト リを作ります。 $ mkdir private_net $ cd private_net Genesis ブロックの作成 ブロックチェーン 上の最初のブロックを Genesis ブロックと言います。プライベートネットワークでは、 Genesis ブロックは自分で作成します。 Genesis ブロックを作成するための元情報として、 Genesis ファイルを作成しましょう。 公式サイトのgenesis.json の中身をコピーして、chainIdを任意の正数にします。今回は、1203にします。 いくつかのchainIdは既に使われているので避けましょう。 { " config ": { " chainId ": 1203 , " homesteadBlock ": 0 , " eip150Block ": 0 , " eip155Block ": 0 , " eip158Block ": 0 , " byzantiumBlock ": 0 , " constantinopleBlock ": 0 , " petersburgBlock ": 0 , " istanbulBlock ": 0 , " berlinBlock ": 0 , " londonBlock ": 0 } , " alloc ": {} , " coinbase ": " 0x0000000000000000000000000000000000000000 ", " difficulty ": " 0x20000 ", " extraData ": "", " gasLimit ": " 0x2fefd8 ", " nonce ": " 0x0000000000000042 ", " mixhash ": " 0x0000000000000000000000000000000000000000000000000000000000000000 ", " parentHash ": " 0x0000000000000000000000000000000000000000000000000000000000000000 ", " timestamp ": " 0x00 " } 作成した genesis . json は、データ ディレクト リに保存します。 次のコマンドで、 Genesis ブロックを作成します。 $ geth init genesis.json INFO [11-29|16:32:24.485] Maximum peer count ETH=50 LES=0 total=50 INFO [11-29|16:32:24.499] Set global gas cap cap=25000000 INFO [11-29|16:32:24.499] Allocated cache and file handles database=/Users/higayasuo/Library/Ethereum/geth/chaindata cache=16.00MiB handles=16 INFO [11-29|16:32:24.576] Persisted trie from memory database nodes=0 size=0.00B time="6.87µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B Fatal: Failed to write genesis block: database contains incompatible genesis (have d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3, new 5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0) ログを見ると、 Failed to write genesis block: と処理に失敗しています。--datadirオプションでデータのための ディレクト リを指定していないので、ライブラリがインストールされている方の ディレクト リに書き込んで失敗しているのでしょう。 今度は、--datadirを指定して、geth initを呼び出します。--datadirで指定した ディレクト リにgethとkeystoreの ディレクト リがあれば、初期化はうまくいっています。 $ geth --datadir . init genesis.json 実は、gethを開発モード(--dev)で起動するといろんなものをあらかじめ準備してくれるので、すぐに送金などを試すことができます。しかし、Ethereumをきちんと理解するには、最初は自分で全部やってみるのが良いと思います。 Gethの起動 先ほど、指定したchainIdをnetworkidに指定して、Gethを起動します。 Gethはデフォルトで自動的に同じネットワークIDのノードを探し接続を試みます。今回、そのような動作は必要ありません。--nodiscoverを指定し無効にします。 $ geth --networkid 1203 --nodiscover ログを見てください。 IPC endpoint opened のログが下のほうにあるはずです。 INFO [11-29|17:03:04.231] IPC endpoint opened url=/Users/higayasuo/Library/Ethereum/geth.ipc geth.ipcが先ほど作成したデータ ディレクト リ(private_net)ではなく、$HOME/Library/Ethereumに作成されています。これは、--datadirオプションを指定し忘れたためです。CTRL-Cでプロセスをいったん止め、次のコマンドで再度起動しましょう。 $ geth --networkid 1203 --nodiscover --datadir . IPC endpoint opened のログを探して、データ ディレクト リ(private_net)にgeth.ipcが作成されたことを確認してください。geth.ipcは、今起動したプロセスに、別のGethがコンソールでアクセスするときに使います。 Gethのconsoleサブコマンドでコンソールとして起動することもできますが、お勧めしません。なぜかというと、Gethのいろんなログがコンソールに表示され使いにくいためです。また、コンソールを終了させるとGeth自身も終了してしまいます。Gethはconsoleなしで起動し、Gethを対話的に操作したい時に別のターミナル(プロセス)から geth attach を使って既に起動したGethのコンソールを立ち上げるのがおすすめです。 Geth attachサブコマンド 別のターミナルを立ち上げ、データ ディレクト リ(private_net)に移動しましょう。Gethのattachサブコマンドで、既に立ち上げたGethプロセスのコンソールを立ち上げることができます。 $ geth attach geth.ipc Welcome to the Geth JavaScript console! instance: Geth/v1.9.25-stable/darwin-amd64/go1.15.6 at block: 0 (Thu Jan 01 1970 09:00:00 GMT+0900 (JST)) datadir: /Users/higayasuo/dev/ethereum/private_net5 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0 To exit, press ctrl-d > Gethは プログラミング言語 Goで作られた CUI ですが、コンソールは JavaScript で出来ているようですね。 geth attach に--datadirをつけ忘れているのではと思う方もいらっしゃるかもしれません。これまでつけ忘れていつも失敗していましたからね。でも、たぶん大丈夫。 geth attach は既に起動したGethプロセスのコンソールなので、そちらに--datadirが付いていれば特に問題はないはず。 etherの採掘 何をやるにしてもまずは元手(ether)が必要です。採掘をしてみましょう。まずは、採掘の状態を確認します。 > eth.mining false まだ、採掘は始まっていませんね。minerモジュールを使って採掘を始めましょう。採掘は非同期に行われます。 > miner.start() Error: etherbase missing: etherbase must be explicitly specified at web3.js:6347:37(47) at web3.js:5081:62(37) at <eval>:1:12(3) etherbaseとは採掘の報酬を受け取るアカウントのことです。現在のアカウントの状態を調べてみましょう。 > eth.accounts [] まだ空っぽです。アカウントを作成しましょう。personalモジュールを使います。 > personal.newAccount() Passphrase: Repeat passphrase: "0x29faad1bb68151278c47df617766bf045c9b2b00" 最後に表示された16進数がアカウントのアドレスです。 eth.accounts をチェックしてみましょう。 > eth.accounts ["0x29faad1bb68151278c47df617766bf045c9b2b00"] もう一つ確認しておきたいのが、 eth.coinbase です。coinbaseはetherbaseと同じものです。 > eth.coinbase "0x29faad1bb68151278c47df617766bf045c9b2b00" coinbaseは eth.accounts[0] が自動で設定されます。 miner.setEtherbase() で eth.accounts[0] 以外のアカウントを設定することもできます。試してみましょう。 > personal.newAccount() Passphrase: Repeat passphrase: "0x0577d82aa10fea504320a087f822d8f68899f980" > eth.accounts ["0x29faad1bb68151278c47df617766bf045c9b2b00", "0x0577d82aa10fea504320a087f822d8f68899f980"] > miner.setEtherbase(eth.accounts[1]) true > eth.coinbase "0x0577d82aa10fea504320a087f822d8f68899f980" 二番目のアカウントが eth.coinbase になっていますね。 etherbaseを変更することが確認できたので、元に戻しておきましょう。 > miner.setEtherbase(eth.accounts[0]) true > eth.coinbase "0x29faad1bb68151278c47df617766bf045c9b2b00" etherbaseが設定できたので、採掘を開始しましょうと言いたいところですが、その前にいくつか確認しておきたいことがあります。一つ目は、 eth.blockNumber 。 > eth.blockNumber 0 0といってもブロックがないわけではなく、 Genesis ブロックのblockNumberが0だということです。blockNumberは採掘が進めば増えていきます。ブロックの中身も eth.getBlock() で確認しておきましょう。logsBloomだけ長いの省略しています。 > eth.getBlock(0) { difficulty: 131072, extraData: "0x", gasLimit: 3141592, gasUsed: 0, hash: "0x5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0", logsBloom: "0x000000000000000000000000000000省略", miner: "0x0000000000000000000000000000000000000000", mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", nonce: "0x0000000000000042", number: 0, parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000", receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 507, stateRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", timestamp: 0, totalDifficulty: 131072, transactions: [], transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: [] } これをみても「これが Genesis ブロックなのか」という実感は湧かないと思いますが、こんなものだと思ってください。アカウントの残高も確認しておきましょう。 > eth.getBalance(eth.accounts[0]) 0 > eth.getBalance(eth.accounts[1]) 0 お待たせしました。採掘を開始しましょう。 > miner.start() null > eth.mining true Gethのメインプロセスのログも見てみましょう。モリモリ採掘しているのが分かります。 eth.blockNumber も確認しましょう。 > eth.blockNumber 127 数が増えているので、きちんと採掘されていることが確認できます。アカウントの残高も確認しておきましょう。 > eth.getBalance(eth.accounts[0]) 2.27e+21 > eth.getBalance(eth.accounts[1]) 0 残高の単位はweiといって、1 ether = 1e+18 weiです。今、1 etherは50万くらいなので、 eth.accounts[0] の残高は1億円超えてますね。 etherの送金 etherの送金は、 eth.sendTransaction() を使います。 from に送金元アカウント、 to に送金先アカウント、 value に送金額をweiで指定します。weiよりetherの方が分かりやすいという場合には、 web3.toWei() を使うといいです。次の例では、2 etherをweiに変換しています。 > web3.toWei(2, 'ether') "2000000000000000000" それでは、 eth.accounts[0] から eth.accounts[1] に 2 ether 送金してみましょう。 > tx = {from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(2, 'ether')} { from: "0x29faad1bb68151278c47df617766bf045c9b2b00", to: "0x0577d82aa10fea504320a087f822d8f68899f980", value: "2000000000000000000" } > eth.sendTransaction(tx) Error: authentication needed: password or unlock at web3.js:6347:37(47) at web3.js:5081:62(37) at <eval>:1:20(4) おや、 Error: authentication needed: password or unlock のエラーが出てしまいました。送金元は、アンロックしておく必要があります。アンロックはデフォルトで5分間持続します。アンロック後、5分以上経過した場合、送金したいなら再度アンロックする必要があります。 personal.unlockAccount() の三番目の引数で、アンロックが持続する時間を秒で指定することもできます。二番目の引数はパスワードなので、パスワードが、見えてしまう問題が生じいまいちですね。 > personal.unlockAccount(eth.accounts[0]) Unlock account 0x29faad1bb68151278c47df617766bf045c9b2b00 Passphrase: true > eth.sendTransaction(tx) "0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0" 最後に表示されたのは、 トランザクション IDです。 eth.getTransaction() で中身を確認してみましょう。 > eth.getTransaction("0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0") { blockHash: "0xe510ee2dc063657b6506b85c78ec361c77d6acc4755b664f2b94faee34b934dd", blockNumber: 4702, from: "0x29faad1bb68151278c47df617766bf045c9b2b00", gas: 21000, gasPrice: 1000000000, hash: "0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0", input: "0x", nonce: 0, r: "0x407f7a5709fc864a8d485cde854c880329475539b773d0e429de0e324f0241c9", s: "0x4998ca73871d2a4dd05374abeb7352f9e8a3c48c1bc288cfe9dbca0ba6206300", to: "0x0577d82aa10fea504320a087f822d8f68899f980", transactionIndex: 0, v: "0x989", value: 2000000000000000000 } from, to, value がきちんと設定されていますね。blockHash, blockNumberが設定されているので、この トランザクション は採掘者によって採掘済みであることがわかります。いつまでたっても、blockHash, blockNumberが設定されない場合は、採掘処理が動いていないかもしれません。 eth.mining で確認して false の場合は、 miner.start() させましょう。 送金先の残高が増えていることを確認しましょう。 web3.fromWei() でweiからetherに単位を変換できます。 > web3.fromWei(eth.getBalance(eth.accounts[1]), 'ether') 2 トランザクション を処理するとき、採掘者に支払われる手数料をgas代といいます。 トランザクション 情報にあったgasの値は、gas代ではありません。 トランザクション 処理時におけるgas使用量の最大値を示しています。 それでは、gas代を知るにはどうしたら良いのでしょうか。送金の時に、送金元の残高から、送金額とgas代が引かれます。そこから計算する必要があります。 eth.accounts[0] は、採掘で常に残高が増え続けているので、gas代を計算するのに向いていません。 eth.accounts[1] から eth.accounts[0] に 1 ether 送金してみましょう。 eth.accounts[1] の現在の残高は、 2 ether ですから 1 ether より少なくなった分がgas代になります。 eth.accounts[1] が送金元になるので、アカウントをアンロックしておく必要があります。失敗から学ぶと言っても同じ失敗を繰り返してはダメですよ。 > personal.unlockAccount(eth.accounts[1]) Unlock account 0x0577d82aa10fea504320a087f822d8f68899f980 Passphrase: true > tx2 = {from: eth.accounts[1], to: eth.accounts[0], value: web3.toWei(1, 'ether')} { from: "0x0577d82aa10fea504320a087f822d8f68899f980", to: "0x29faad1bb68151278c47df617766bf045c9b2b00", value: "1000000000000000000" } > txId2 = eth.sendTransaction(tx2) "0xf9c427c253bfd6980c236a691393e1cc501b1d3d71deaf5ddcf17f2d3fca206f" > txInfo2 = eth.getTransaction(txId2) { blockHash: "0xbf4c0d1ccc2325ad2ada70148b818724119ba582d4f0f765b3f56edc36cabc4f", blockNumber: 6378, from: "0x0577d82aa10fea504320a087f822d8f68899f980", gas: 21000, gasPrice: 1000000000, hash: "0xf9c427c253bfd6980c236a691393e1cc501b1d3d71deaf5ddcf17f2d3fca206f", input: "0x", nonce: 0, r: "0x2703788aaad9dd2ae6d08802ae555cdbdbdd1a6dfc432aa94fd1ccc5f4efe8a0", s: "0x413213c0447df661f8ef204e21654ff41894d219a3181f17cad30895aea4034e", to: "0x29faad1bb68151278c47df617766bf045c9b2b00", transactionIndex: 0, v: "0x98a", value: 1000000000000000000 } > gasFee2 = web3.toWei(1, 'ether') - eth.getBalance(eth.accounts[1]) 21000000000000 > gas2 = gasFee2 / txInfo2.gasPrice 21000 gasFee2が今回の トランザクション のgas代です。gasPriceは1gasあたりの手数料です。gas代をgasPriceで割ると今回、何gas使用したかが分かります。 今回はここまで。Gethを止めておきましょう。コンソールはCTL-D。メインプロセスはCTL-Cで止めます。 次回は、スマート コントラ クトを扱います。 僕の書いたNFT関連の記事 NFT入門 スマートコントラクト入門 執筆: @higa 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
これは 電通国際情報サービス アドベントカレンダー の3日目の記事です。 こんにちは。 電通国際情報サービス (ISID) イノベーション ラボの比嘉です。 うちの会社が、テックブログを始めるということなので、僕もブログを再開します。以前は個人のブログだったので、技術以外のエントリもいろいろありましたが、今後は会社のテックブログとしてやっていきます。 ブログでは基本的にEthereumのプログラミングについて書いていく予定です。僕自身、Ethereumについてほとんど知らないので、学んでいく過程をブログに書いていきます。また、できるだけ意図的にエラーを起こし、それを考察していこうと思います。失敗から学べることは多いからね。 Gethとは Gethのインストール MacへのGethのインストール WindowsへのGethのインストール プライベートネットワークへの接続 データディレクトリの作成 Genesisブロックの作成 Gethの起動 Geth attachサブコマンド etherの採掘 etherの送金 Gethとは Ethereumを利用する場合、まずはEthereumのネットワークに参加する必要があります。ネットワークへの参加には、Ethereumクライアントを使います。 Ethereumクライアントの中で公式に推奨されているクライアントがGethです。Gethは プログラミング言語 Goにより実装された CUI クライアントです。Gethを使うことで次のようなことができます。 etherの採掘 etherの送金 スマート・ コントラ クトの生成 Gethのインストール Gethのインストールの公式サイトはこちら Mac と Windows については、載せておきます。 Mac しか試していません。ごめんなさい。 Mac へのGethのインストール Homebrewを使うのが簡単です。Homebrewをインストールしてから次のコマンドを実行してください。 $ brew tap ethereum/ethereum $ brew install ethereum Windows へのGethのインストール インストーラ ーをダウンロードして実行しましょう。 ダウンロードサイトはこちら プライベートネットワークへの接続 Ethereum 本番環境ネットワークにいきなり接続するのは怖いですよね。まだ、Ethereumのこともよく知らないし。まずは、自分だけの閉じたネットワーク(プライベートネットワーク)を作成してそこに接続しましょう。 データ ディレクト リの作成 データを格納する ディレクト リを作って、そこに移動します。例として、private_netという ディレクト リを作ります。 $ mkdir private_net $ cd private_net Genesis ブロックの作成 ブロックチェーン 上の最初のブロックを Genesis ブロックと言います。プライベートネットワークでは、 Genesis ブロックは自分で作成します。 Genesis ブロックを作成するための元情報として、 Genesis ファイルを作成しましょう。 公式サイトのgenesis.json の中身をコピーして、chainIdを任意の正数にします。今回は、1203にします。 いくつかのchainIdは既に使われているので避けましょう。 { " config ": { " chainId ": 1203 , " homesteadBlock ": 0 , " eip150Block ": 0 , " eip155Block ": 0 , " eip158Block ": 0 , " byzantiumBlock ": 0 , " constantinopleBlock ": 0 , " petersburgBlock ": 0 , " istanbulBlock ": 0 , " berlinBlock ": 0 , " londonBlock ": 0 } , " alloc ": {} , " coinbase ": " 0x0000000000000000000000000000000000000000 ", " difficulty ": " 0x20000 ", " extraData ": "", " gasLimit ": " 0x2fefd8 ", " nonce ": " 0x0000000000000042 ", " mixhash ": " 0x0000000000000000000000000000000000000000000000000000000000000000 ", " parentHash ": " 0x0000000000000000000000000000000000000000000000000000000000000000 ", " timestamp ": " 0x00 " } 作成した genesis . json は、データ ディレクト リに保存します。 次のコマンドで、 Genesis ブロックを作成します。 $ geth init genesis.json INFO [11-29|16:32:24.485] Maximum peer count ETH=50 LES=0 total=50 INFO [11-29|16:32:24.499] Set global gas cap cap=25000000 INFO [11-29|16:32:24.499] Allocated cache and file handles database=/Users/higayasuo/Library/Ethereum/geth/chaindata cache=16.00MiB handles=16 INFO [11-29|16:32:24.576] Persisted trie from memory database nodes=0 size=0.00B time="6.87µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B Fatal: Failed to write genesis block: database contains incompatible genesis (have d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3, new 5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0) ログを見ると、 Failed to write genesis block: と処理に失敗しています。--datadirオプションでデータのための ディレクト リを指定していないので、ライブラリがインストールされている方の ディレクト リに書き込んで失敗しているのでしょう。 今度は、--datadirを指定して、geth initを呼び出します。--datadirで指定した ディレクト リにgethとkeystoreの ディレクト リがあれば、初期化はうまくいっています。 $ geth --datadir . init genesis.json 実は、gethを開発モード(--dev)で起動するといろんなものをあらかじめ準備してくれるので、すぐに送金などを試すことができます。しかし、Ethereumをきちんと理解するには、最初は自分で全部やってみるのが良いと思います。 Gethの起動 先ほど、指定したchainIdをnetworkidに指定して、Gethを起動します。 Gethはデフォルトで自動的に同じネットワークIDのノードを探し接続を試みます。今回、そのような動作は必要ありません。--nodiscoverを指定し無効にします。 $ geth --networkid 1203 --nodiscover ログを見てください。 IPC endpoint opened のログが下のほうにあるはずです。 INFO [11-29|17:03:04.231] IPC endpoint opened url=/Users/higayasuo/Library/Ethereum/geth.ipc geth.ipcが先ほど作成したデータ ディレクト リ(private_net)ではなく、$HOME/Library/Ethereumに作成されています。これは、--datadirオプションを指定し忘れたためです。CTRL-Cでプロセスをいったん止め、次のコマンドで再度起動しましょう。 $ geth --networkid 1203 --nodiscover --datadir . IPC endpoint opened のログを探して、データ ディレクト リ(private_net)にgeth.ipcが作成されたことを確認してください。geth.ipcは、今起動したプロセスに、別のGethがコンソールでアクセスするときに使います。 Gethのconsoleサブコマンドでコンソールとして起動することもできますが、お勧めしません。なぜかというと、Gethのいろんなログがコンソールに表示され使いにくいためです。また、コンソールを終了させるとGeth自身も終了してしまいます。Gethはconsoleなしで起動し、Gethを対話的に操作したい時に別のターミナル(プロセス)から geth attach を使って既に起動したGethのコンソールを立ち上げるのがおすすめです。 Geth attachサブコマンド 別のターミナルを立ち上げ、データ ディレクト リ(private_net)に移動しましょう。Gethのattachサブコマンドで、既に立ち上げたGethプロセスのコンソールを立ち上げることができます。 $ geth attach geth.ipc Welcome to the Geth JavaScript console! instance: Geth/v1.9.25-stable/darwin-amd64/go1.15.6 at block: 0 (Thu Jan 01 1970 09:00:00 GMT+0900 (JST)) datadir: /Users/higayasuo/dev/ethereum/private_net5 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0 To exit, press ctrl-d > Gethは プログラミング言語 Goで作られた CUI ですが、コンソールは JavaScript で出来ているようですね。 geth attach に--datadirをつけ忘れているのではと思う方もいらっしゃるかもしれません。これまでつけ忘れていつも失敗していましたからね。でも、たぶん大丈夫。 geth attach は既に起動したGethプロセスのコンソールなので、そちらに--datadirが付いていれば特に問題はないはず。 etherの採掘 何をやるにしてもまずは元手(ether)が必要です。採掘をしてみましょう。まずは、採掘の状態を確認します。 > eth.mining false まだ、採掘は始まっていませんね。minerモジュールを使って採掘を始めましょう。採掘は非同期に行われます。 > miner.start() Error: etherbase missing: etherbase must be explicitly specified at web3.js:6347:37(47) at web3.js:5081:62(37) at <eval>:1:12(3) etherbaseとは採掘の報酬を受け取るアカウントのことです。現在のアカウントの状態を調べてみましょう。 > eth.accounts [] まだ空っぽです。アカウントを作成しましょう。personalモジュールを使います。 > personal.newAccount() Passphrase: Repeat passphrase: "0x29faad1bb68151278c47df617766bf045c9b2b00" 最後に表示された16進数がアカウントのアドレスです。 eth.accounts をチェックしてみましょう。 > eth.accounts ["0x29faad1bb68151278c47df617766bf045c9b2b00"] もう一つ確認しておきたいのが、 eth.coinbase です。coinbaseはetherbaseと同じものです。 > eth.coinbase "0x29faad1bb68151278c47df617766bf045c9b2b00" coinbaseは eth.accounts[0] が自動で設定されます。 miner.setEtherbase() で eth.accounts[0] 以外のアカウントを設定することもできます。試してみましょう。 > personal.newAccount() Passphrase: Repeat passphrase: "0x0577d82aa10fea504320a087f822d8f68899f980" > eth.accounts ["0x29faad1bb68151278c47df617766bf045c9b2b00", "0x0577d82aa10fea504320a087f822d8f68899f980"] > miner.setEtherbase(eth.accounts[1]) true > eth.coinbase "0x0577d82aa10fea504320a087f822d8f68899f980" 二番目のアカウントが eth.coinbase になっていますね。 etherbaseを変更することが確認できたので、元に戻しておきましょう。 > miner.setEtherbase(eth.accounts[0]) true > eth.coinbase "0x29faad1bb68151278c47df617766bf045c9b2b00" etherbaseが設定できたので、採掘を開始しましょうと言いたいところですが、その前にいくつか確認しておきたいことがあります。一つ目は、 eth.blockNumber 。 > eth.blockNumber 0 0といってもブロックがないわけではなく、 Genesis ブロックのblockNumberが0だということです。blockNumberは採掘が進めば増えていきます。ブロックの中身も eth.getBlock() で確認しておきましょう。logsBloomだけ長いの省略しています。 > eth.getBlock(0) { difficulty: 131072, extraData: "0x", gasLimit: 3141592, gasUsed: 0, hash: "0x5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0", logsBloom: "0x000000000000000000000000000000省略", miner: "0x0000000000000000000000000000000000000000", mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", nonce: "0x0000000000000042", number: 0, parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000", receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 507, stateRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", timestamp: 0, totalDifficulty: 131072, transactions: [], transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: [] } これをみても「これが Genesis ブロックなのか」という実感は湧かないと思いますが、こんなものだと思ってください。アカウントの残高も確認しておきましょう。 > eth.getBalance(eth.accounts[0]) 0 > eth.getBalance(eth.accounts[1]) 0 お待たせしました。採掘を開始しましょう。 > miner.start() null > eth.mining true Gethのメインプロセスのログも見てみましょう。モリモリ採掘しているのが分かります。 eth.blockNumber も確認しましょう。 > eth.blockNumber 127 数が増えているので、きちんと採掘されていることが確認できます。アカウントの残高も確認しておきましょう。 > eth.getBalance(eth.accounts[0]) 2.27e+21 > eth.getBalance(eth.accounts[1]) 0 残高の単位はweiといって、1 ether = 1e+18 weiです。今、1 etherは50万くらいなので、 eth.accounts[0] の残高は1億円超えてますね。 etherの送金 etherの送金は、 eth.sendTransaction() を使います。 from に送金元アカウント、 to に送金先アカウント、 value に送金額をweiで指定します。weiよりetherの方が分かりやすいという場合には、 web3.toWei() を使うといいです。次の例では、2 etherをweiに変換しています。 > web3.toWei(2, 'ether') "2000000000000000000" それでは、 eth.accounts[0] から eth.accounts[1] に 2 ether 送金してみましょう。 > tx = {from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(2, 'ether')} { from: "0x29faad1bb68151278c47df617766bf045c9b2b00", to: "0x0577d82aa10fea504320a087f822d8f68899f980", value: "2000000000000000000" } > eth.sendTransaction(tx) Error: authentication needed: password or unlock at web3.js:6347:37(47) at web3.js:5081:62(37) at <eval>:1:20(4) おや、 Error: authentication needed: password or unlock のエラーが出てしまいました。送金元は、アンロックしておく必要があります。アンロックはデフォルトで5分間持続します。アンロック後、5分以上経過した場合、送金したいなら再度アンロックする必要があります。 personal.unlockAccount() の三番目の引数で、アンロックが持続する時間を秒で指定することもできます。二番目の引数はパスワードなので、パスワードが、見えてしまう問題が生じいまいちですね。 > personal.unlockAccount(eth.accounts[0]) Unlock account 0x29faad1bb68151278c47df617766bf045c9b2b00 Passphrase: true > eth.sendTransaction(tx) "0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0" 最後に表示されたのは、 トランザクション IDです。 eth.getTransaction() で中身を確認してみましょう。 > eth.getTransaction("0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0") { blockHash: "0xe510ee2dc063657b6506b85c78ec361c77d6acc4755b664f2b94faee34b934dd", blockNumber: 4702, from: "0x29faad1bb68151278c47df617766bf045c9b2b00", gas: 21000, gasPrice: 1000000000, hash: "0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0", input: "0x", nonce: 0, r: "0x407f7a5709fc864a8d485cde854c880329475539b773d0e429de0e324f0241c9", s: "0x4998ca73871d2a4dd05374abeb7352f9e8a3c48c1bc288cfe9dbca0ba6206300", to: "0x0577d82aa10fea504320a087f822d8f68899f980", transactionIndex: 0, v: "0x989", value: 2000000000000000000 } from, to, value がきちんと設定されていますね。blockHash, blockNumberが設定されているので、この トランザクション は採掘者によって採掘済みであることがわかります。いつまでたっても、blockHash, blockNumberが設定されない場合は、採掘処理が動いていないかもしれません。 eth.mining で確認して false の場合は、 miner.start() させましょう。 送金先の残高が増えていることを確認しましょう。 web3.fromWei() でweiからetherに単位を変換できます。 > web3.fromWei(eth.getBalance(eth.accounts[1]), 'ether') 2 トランザクション を処理するとき、採掘者に支払われる手数料をgas代といいます。 トランザクション 情報にあったgasの値は、gas代ではありません。 トランザクション 処理時におけるgas使用量の最大値を示しています。 それでは、gas代を知るにはどうしたら良いのでしょうか。送金の時に、送金元の残高から、送金額とgas代が引かれます。そこから計算する必要があります。 eth.accounts[0] は、採掘で常に残高が増え続けているので、gas代を計算するのに向いていません。 eth.accounts[1] から eth.accounts[0] に 1 ether 送金してみましょう。 eth.accounts[1] の現在の残高は、 2 ether ですから 1 ether より少なくなった分がgas代になります。 eth.accounts[1] が送金元になるので、アカウントをアンロックしておく必要があります。失敗から学ぶと言っても同じ失敗を繰り返してはダメですよ。 > personal.unlockAccount(eth.accounts[1]) Unlock account 0x0577d82aa10fea504320a087f822d8f68899f980 Passphrase: true > tx2 = {from: eth.accounts[1], to: eth.accounts[0], value: web3.toWei(1, 'ether')} { from: "0x0577d82aa10fea504320a087f822d8f68899f980", to: "0x29faad1bb68151278c47df617766bf045c9b2b00", value: "1000000000000000000" } > txId2 = eth.sendTransaction(tx2) "0xf9c427c253bfd6980c236a691393e1cc501b1d3d71deaf5ddcf17f2d3fca206f" > txInfo2 = eth.getTransaction(txId2) { blockHash: "0xbf4c0d1ccc2325ad2ada70148b818724119ba582d4f0f765b3f56edc36cabc4f", blockNumber: 6378, from: "0x0577d82aa10fea504320a087f822d8f68899f980", gas: 21000, gasPrice: 1000000000, hash: "0xf9c427c253bfd6980c236a691393e1cc501b1d3d71deaf5ddcf17f2d3fca206f", input: "0x", nonce: 0, r: "0x2703788aaad9dd2ae6d08802ae555cdbdbdd1a6dfc432aa94fd1ccc5f4efe8a0", s: "0x413213c0447df661f8ef204e21654ff41894d219a3181f17cad30895aea4034e", to: "0x29faad1bb68151278c47df617766bf045c9b2b00", transactionIndex: 0, v: "0x98a", value: 1000000000000000000 } > gasFee2 = web3.toWei(1, 'ether') - eth.getBalance(eth.accounts[1]) 21000000000000 > gas2 = gasFee2 / txInfo2.gasPrice 21000 gasFee2が今回の トランザクション のgas代です。gasPriceは1gasあたりの手数料です。gas代をgasPriceで割ると今回、何gas使用したかが分かります。 今回はここまで。Gethを止めておきましょう。コンソールはCTL-D。メインプロセスはCTL-Cで止めます。 次回は、スマート コントラ クトを扱います。 僕の書いたNFT関連の記事 NFT入門 スマートコントラクト入門 執筆: @higa 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
はいどーもー! X イノベーション 本部の宮澤響です! 本記事は 電通国際情報サービス Advent Calendar 2021 2日目の記事です! 記念すべき1日目である昨日の記事は、佐藤太一さんの「 テックブログ始めました。 」でした!我々の記事をホストするためのサービスとして はてなブログ を採用した理由や、記事を執筆するにあたって利用しているツールについて分かりやすくまとめられていますので、ぜひご一読ください! 本記事では、「 GitHub Actionsと AWS App Runnerを利用してBlue/Greenデプロイメントを実現してみた」というタイトルのとおり、 GitHub Actionsと AWS App Runner(以下、App Runner)、それに加えて、 Amazon CloudFront (以下、CloudFront)と Amazon Route 53(以下、Route 53)を利用してBlue/Greenデプロイメントを実現する方法を、サンプルコードとともにご紹介します! Blue/Greenデプロイメントって何? App Runnerって何? 事前に準備すること App Runnerサービスを作成する CloudFrontディストリビューションを作成する Route 53のレコードを作成する GitHubのRepository secretsを設定する GitHub ActionsのワークフローのYAMLファイルを作成する 実際にBlue/Greenデプロイメントしてみる GitHub ActionsのワークフローのYAMLファイルの改善方法 GitHub Actionsのログ出力の制御 OpenID Connectを利用したAWSリソースへのアクセス CloudFrontディストリビューションのキャッシュパージ まとめ Blue/Greenデプロイメントって何? Blue/Greenデプロイメントとは、本番環境と検証環境を交互に入れ替えることにより、Webアプリケーションなどに変更を加えるデプロイ手法の一つです。ざっくりとした流れとしては、「本番環境であるBlue」と「本番環境とほぼ同一の検証環境Green」の2つを用意した上で、以下の1.〜4.を繰り返すことになります。 Greenに変更を加えていく 問題がなければGreenを本番環境に、Blueを検証環境にチェンジする(デプロイ) 今度はBlueに変更を加えていく 問題がなければ再びBlueを本番環境に、Greenを検証環境にチェンジする(デプロイ) メリットとしては、デプロイ時や ロールバック 時のシステムのダウンタイムを最小限にできることや、限りなく本番環境に近い検証環境でテストできることなどが挙げられます。 一方、デメリットとしては、 アーキテクチャ が複雑になることや、インフラ構築用のコードと実際のインフラ環境に矛盾が生じる(インフラ構築用のコード上でBlueを本番環境としていた場合、Greenが本番環境になっている間はインフラ構築用のコードと実際のインフラ環境が一致しない)ことなどが挙げられます。 App Runnerって何? App Runnerとは、コンテナベースの AWS リソースの一つです。噛み砕いて言えば、 Amazon ECS、 AWS Fargate、ELBといった種々のリソースを全部まとめて裏でイイ感じにやってくれるものです。非常に手軽で簡単にWebアプリケーションをデプロイできる反面、制約も多いです。詳しくは 公式ドキュメント を参照ください。 事前に準備すること 各種 AWS リソースや、Blue/Greenデプロイメントを実現するための GitHub Actionsのワークフローを作成します。最終的な アーキテクチャ は下図になります。 App Runnerサービスを作成する まずは、 公式ドキュメント の手順に従い、サンプルアプリケーションがデプロイされているApp Runnerサービスを2つ作成します。 Prerequisites の手順に従い、サンプル リポジトリ を作成します。 リポジトリ 名は任意でOKです。 ステップ 1: App Runner サービスを作成する の手順に従い、App Runnerサービスを作成します。 GitHub connectionsの接続名や 環境変数 NAME の値は任意でOKです。サービス名はそれぞれ sample-service-blue 、 sample-service-green とします。また、デプロイ設定の部分で、 sample-service-blue のデプロイトリガーを 手動 に、 sample-service-green のデプロイトリガーを 自動 に、それぞれ設定してください。 何故片方を手動デプロイ、もう片方を自動デプロイにするかというと、自動デプロイの場合、指定した リポジトリ 、ブランチの ソースコード に変更を加える(=pushする)度に、App Runnerサービスにも変更が反映される(=最新の ソースコード を基にアプリケーションがデプロイし直される)ためです。これにより、本番環境(手動デプロイ)に影響を与えることなく、検証環境(自動デプロイ)に変更を加えることが可能となります。 ソースコード をpushするだけで最新のアプリケーションが自動でデプロイされるのは非常に手軽で便利ですね! CloudFront ディストリビューション を作成する 次に、 公式ドキュメント の手順に従い、CloudFront ディストリビューション を2つ作成します。基本的にはデフォルト設定のままで問題ありませんが、以下の項目はデフォルトから変更をお願いします。 2つの ディストリビューション 共通の設定 項目 値 プロトコル HTTPSのみ 料金クラス 北米、欧州、アジア、中東、アフリカを使用 (Blue/Greenデプロイメントには直接関係ありませんが、コストを抑えるためです) カスタム SSL証明書 任意の証明書 (本記事では example.com とします) それぞれの ディストリビューション で異なる設定 項目 1つ目の ディストリビューション に設定する値 2つ目の ディストリビューション に設定する値 オリジン ドメイン sample-service-blue のデフォルト ドメイン ( https:// の部分は不要です) sample-service-green のデフォルト ドメイン ( https:// の部分は不要です) 代替 ドメイン 名 カスタム SSL証明書 に対応する任意の ドメイン 名 (本記事では sample.example.com とします) なし 説明 sample-distribution-blue sample-distribution-green 何故片方だけに代替 ドメイン 名を入力するかというと、CloudFrontのルールとして、複数の ディストリビューション に同一の代替 ドメイン を同時に設定できないようになっているためです。つまり、代替 ドメイン は、常にルーティング先が本番環境になっている方の ディストリビューション にのみ設定されるように、適宜付け替える必要があります。 Route 53のレコードを作成する 続いて、 公式ドキュメント の手順に従い、Route 53のレコードを2つ作成します。 1つ目(Aレコード) 項目 値 レコード名 sample-distribution-blue の代替 ドメイン 名と対応する名称 (本記事では sample とします) レコードタイプ A の エイリアス トラフィック のルーティング先 CloudFrontディストリビューションへのエイリアス > sample-distribution-blue の ドメイン 名 ルーティングポリシー シンプルルーティング 2つ目(TXTレコード) 項目 値 レコード名 Aレコードのレコード名の先頭に _ を付したもの (本記事では _sample とします) レコードタイプ TXT トラフィック のルーティング先 sample-distribution-green の ドメイン 名の末尾に . を付したもの ルーティングポリシー シンプルルーティング 何故2つ目のTXTレコードが必要になるかというと、 CloudFrontディストリビューションを作成する の節で説明した代替 ドメイン の付け替えに必要になるためです。このレコードに、代替 ドメイン を設定しない方の ディストリビューション の ドメイン を設定しておく必要があります。レコード名の先頭に _ を付けたり値の末尾に . を付けたりするのは仕様です。詳しくは 公式ドキュメント を参照ください。 GitHub のRepository secretsを設定する ここからは GitHub 側の準備になります。 GitHub Actionsの利用に際して、 AWS の認証情報や間接的に取得することが難しい値を、あらかじめRepository secretsに設定しておきます。 公式ドキュメント の手順に従い、以下の4つを設定してください。 Name Value AWS_ACCESS_KEY_ID 自身の AWS アクセスキーID AWS_SECRET_ACCESS_KEY 自身の AWS シークレットアクセスキー AWS_ROUTE53_HOSTED_ZONE_ID Route 53のホストゾーンID AWS_ROUTE53_RECORD_NAME sample.example.com (Route 53のAレコードのレコード名) GitHub Actionsのワークフローの YAML ファイルを作成する 最後に、実際にBlue/Greenデプロイメントを実現する部分である、 GitHub Actionsのワークフローの YAML ファイルを作成します。今回の構成でBlue/Greenデプロイメントの実現に必要な要素は、以下の4つです。 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する CloudFront ディストリビューション の代替 ドメイン 名を付け替える Route 53のAレコードとTXTレコードのルーティング先を入れ替える まずはこれらを、 AWS CLI のコマンドを用いて GitHub Actionsのワークフローに落とし込みます。なお、以下の例では、 release/〇〇 のようなタグのpushをBlue/Greenデプロイメントのトリガーとしています。 name : Blue/Green Deploy on : push : tags : - release/* jobs : deploy : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 # 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する - name : Change Source Configuration of Current Service run : aws apprunner update-service --service-arn `本番環境側のApp RunnerサービスのARN` --source-configuration `設定ファイルのパス` # 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する - name : Change Source Configuration of Next Service run : aws apprunner update-service --service-arn `検証環境側のApp RunnerサービスのARN` --source-configuration `設定ファイルのパス` # CloudFrontディストリビューションの代替ドメイン名を付け替える - name : Replace Alias run : aws cloudfront associate-alias --target-distribution-id `検証環境側のCloudFrontディストリビューションのID` --alias ${{ secrets.AWS_ROUTE53_RECORD_NAME }} # Route 53のAレコードとTXTレコードのルーティング先を入れ替える - name : Change Record Targets run : aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --change-batch `設定ファイルのパス` このワークフローを実行することにより、現在の検証環境が本番環境となり、Aレコードの ドメイン からアクセス可能になります。一方、現在の本番環境は検証環境となり、 ソースコード をpushするだけで最新のアプリケーションが自動でデプロイされます。 しかしながら、このままでは現在の本番環境がどちらかをその都度手動で調べたり、各リソースのARNやIDなどをコピペしたり、設定ファイルを自作したりする必要があります。当然、そんなことをしていては非常に面倒ですし、ヒューマンエラーも発生しやすくなってしまいます。 そのため、それらの処理も併せて自動化したものが以下になります。 name : Blue/Green Deploy on : push : tags : - release/* jobs : deploy : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 # CloudFrontディストリビューションの一覧を取得し、その中から説明が`sample-distribution`で始まっているものの情報をファイルに書き込む # 代替ドメインが1個のものを現在の本番環境、0個のものを検証環境と判定する - name : Get Distribution List run : | aws cloudfront list-distributions | jq '.DistributionList.Items[] | select(.Comment | startswith("sample-distribution"))' > distribution-list.json cat distribution-list.json | jq 'select(.Aliases.Quantity==1)' > current-distribution.json cat distribution-list.json | jq 'select(.Aliases.Quantity==0)' > next-distribution.json # 2つのCloudFrontディストリビューションのドメイン名を取得し、変数に代入する - name : Get Distribution Domains id : distribution-domains run : | echo "::set-output name=CURRENT_DISTRIBUTION_DOMAIN::$(cat current-distribution.json | jq -r '.DomainName')" echo "::set-output name=NEXT_DISTRIBUTION_DOMAIN::$(cat next-distribution.json | jq -r '.DomainName')" # 検証環境側のCloudFrontディストリビューションのIDを取得し、変数に代入する - name : Get Distribution ID id : distribution-id run : echo "::set-output name=NEXT_DISTRIBUTION_ID::$(cat next-distribution.json | jq -r '.Id')" # App Runnerサービスの一覧を取得し、ファイルに書き込む - name : Get Service List run : aws apprunner list-services | jq '.ServiceSummaryList[]' > service-list.json # 2つのApp Runnerサービスのドメインを取得し、変数に代入する - name : Get Service Domains id : service-domains run : | echo "::set-output name=CURRENT_SERVICE_DOMAIN::$(cat current-distribution.json | jq -r '.Origins.Items[0].DomainName')" echo "::set-output name=NEXT_SERVICE_DOMAIN::$(cat next-distribution.json | jq -r '.Origins.Items[0].DomainName')" # 2つのApp RunnerサービスのARNを取得し、変数に代入する - name : Get Service ARNs id : service-arns run : | echo "::set-output name=CURRENT_SERVICE_ARN::$(cat service-list.json | jq -r --arg domain ${{ steps.service-domains.outputs.CURRENT_SERVICE_DOMAIN }} 'select(.ServiceUrl==$domain) | .ServiceArn')" echo "::set-output name=NEXT_SERVICE_ARN::$(cat service-list.json | jq -r --arg domain ${{ steps.service-domains.outputs.NEXT_SERVICE_DOMAIN }} 'select(.ServiceUrl==$domain) | .ServiceArn')" # 2つのApp Runnerサービスの設定情報を取得し、デプロイトリガー部分を変更した上でファイルに書き込む - name : Get Service Config Files run : | aws apprunner describe-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} | jq '.Service.SourceConfiguration | .AutoDeploymentsEnabled=true' > current-service-config.json aws apprunner describe-service --service-arn ${{ steps.service-arns.outputs.NEXT_SERVICE_ARN }} | jq '.Service.SourceConfiguration | .AutoDeploymentsEnabled=false' > next-service-config.json # 指定したホストゾーンIDのRoute 53のレコードの一覧を取得し、指定したレコード名のものと、指定したレコード名の先頭に`_`を付したものの情報をファイルに書き込む # 2つのレコードの設定部分を変更した上でファイルに書き込む - name : Get Record Config File run : | aws route53 list-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --output json | jq --arg name ${{ secrets.AWS_ROUTE53_RECORD_NAME }} '.ResourceRecordSets[] | select(.Name | endswith($name+"."))' > record-list.json cat record-list.json | jq --arg domain ${{ steps.distribution-domains.outputs.CURRENT_DISTRIBUTION_DOMAIN }} 'select(.Type=="TXT") | .ResourceRecords[0].Value="\""+$domain+".\"" | {"Changes":[{"Action":"UPSERT"}+{ResourceRecordSet:.}]}' > txt-record.json cat record-list.json | jq --arg domain ${{ steps.distribution-domains.outputs.NEXT_DISTRIBUTION_DOMAIN }} 'select(.Type=="A") | .AliasTarget.DNSName=$domain+"." | {"Changes":[{"Action":"UPSERT"}+{ResourceRecordSet:.}]}' > a-record.json cat txt-record.json a-record.json | jq -s '.[0].Changes+.[1].Changes | {"Changes":.}' > record-config.json # 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する - name : Change Source Configuration of Current Service run : aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} --source-configuration file://current-service-config.json # 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する - name : Change Source Configuration of Next Service run : aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.NEXT_SERVICE_ARN }} --source-configuration file://next-service-config.json # CloudFrontディストリビューションの代替ドメイン名を付け替える - name : Replace Alias run : aws cloudfront associate-alias --target-distribution-id ${{ steps.distribution-id.outputs.NEXT_DISTRIBUTION_ID }} --alias ${{ secrets.AWS_ROUTE53_RECORD_NAME }} # Route 53のAレコードとTXTレコードのルーティング先を入れ替える - name : Change Record Targets run : aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --change-batch file://record-config.json # 作成したファイルを削除する - name : Delete Temporary Files if : ${{ always() }} run : rm distribution-list.json current-distribution.json next-distribution.json service-list.json current-service-config.json next-service-config.json record-list.json txt-record.json a-record.json record-config.json 実際にBlue/Greenデプロイメントしてみる それでは、実際にBlue/Greenデプロイメントのワークフローを動作させ、環境の切り替わりを確認してみます。 現在の本番環境の状態を確認するために、 sample.example.com にアクセスします。 サンプルアプリケーションの文字列がそのまま表示されます。 現在の検証環境の状態を確認するために、 sample-distribution-green の ドメイン にアクセスします。 本番環境と同一の画面が表示されます。 server.py の8行目の Hello を こんにちは に変更して、 GitHub リポジトリ にpushします。 MESSAGE = "こんにちは, " + name + "!" sample-service-green へのデプロイが完了するのを待ってから、再度 sample-distribution-green の ドメイン にアクセスしてみます。 こんにちは に更新されているため、検証環境には先ほどpushした内容が反映されていることが分かります。 再度 sample.example.com にアクセスしてみます。 Hello のままであるため、先ほどのpushが本番環境には影響を与えていないことが分かります。 release/v1.0.0 というタグをpushします。 GitHub Actionsのワークフローが正常に完了していれば成功です。 git tag release/v1.0.0 git push origin release/v1.0.0 再度 sample.example.com にアクセスしてみます。 こんにちは に更新されています。Blue/Greenデプロイメントにより、先ほどまでの検証環境が数十秒のうちに本番環境に切り替わったことが確認できました! なお、確認は省略しますが、今回確認したBlueからGreenへの切り替えだけでなく、GreenからBlueへの切り替えも正常に動作します。 GitHub Actionsのワークフローの YAML ファイルの改善方法 GitHub Actionsのログ出力の制御 本記事にサンプルとして掲載した YAML ファイルによるワークフローを実行すると、Repository secrets以外の変数の値や AWS CLI のコマンドの実行結果が GitHub Actionsのログに出力されてしまいます。そのため、Publicな リポジトリ で実行する場合には、 ログ中での値のマスク や nul へのリダイレクトなどを利用して、ログの出力を工夫する必要があります。 # 値のマスクの例 CURRENT_DISTRIBUTION_DOMAIN=$(cat current-distribution.json | jq -r '.DomainName') echo "::add-mask::$CURRENT_DISTRIBUTION_DOMAIN" echo "::set-output name=CURRENT_DISTRIBUTION_DOMAIN::$CURRENT_DISTRIBUTION_DOMAIN" # nulへのリダイレクトの例 aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} --source-configuration file://current-service-config.json > nul OpenID Connectを利用した AWS リソースへのアクセス GitHub Actionsから AWS リソースにアクセスする方法に関しては、 OpenID Connectを利用する方法が先日発表されました。こちらの方法を利用すると、 AWS アクセスキーIDや AWS シークレットアクセスキーを利用せずに AWS リソースにアクセスできるため、より安全性を高められます。詳しくは、 公式ドキュメント や、テックブログ記事である OpenID Connectを利用してGitHub ActionsからAWSリソースにアクセスする を参照ください。 CloudFront ディストリビューション のキャッシュパージ Blue/Greenデプロイメントのワークフローの YAML ファイルとは別に、 main ブランチへのpushをトリガーとして検証環境側のCloudFront ディストリビューション のキャッシュをパージするワークフローの YAML ファイルを作成することにより、アプリケーションをデプロイし直す際にキャッシュによって古い情報が配信されることを防げます。 # キャッシュパージの例 NEXT_DISTRIBUTION_ID=$(aws cloudfront list-distributions | jq -r '.DistributionList.Items[] | select(.Comment | startswith("sample-distribution")) | select(.Aliases.Quantity==0) | .Id') aws cloudfront create-invalidation --distribution-id $NEXT_DISTRIBUTION_ID --paths "/*" まとめ 本記事では、 GitHub Actions、App Runner、CloudFront、Route 53を利用してBlue/Greenデプロイメントを実現する方法をご紹介しました。 GitHub リポジトリ に ソースコード をpushするだけで検証環境にアプリケーションがデプロイされ、タグをpushするだけでBlue/Greenデプロイメントが完了するというのは、開発活動を進めていく上で非常に便利で快適です。機会があれば皆さんもぜひお試しください! 電通国際情報サービス Advent Calendar 2021 3日目となる明日の記事は 比嘉康雄 さんの「 Geth(ゲス)はじめました 」です!お楽しみに! 最後までお読みいただき、本当にありがとうございました! 執筆: @miyazawa.hibiki 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
はいどーもー! X イノベーション 本部の宮澤響です! 本記事は 電通国際情報サービス Advent Calendar 2021 2日目の記事です! 記念すべき1日目である昨日の記事は、佐藤太一さんの「 テックブログ始めました。 」でした!我々の記事をホストするためのサービスとして はてなブログ を採用した理由や、記事を執筆するにあたって利用しているツールについて分かりやすくまとめられていますので、ぜひご一読ください! 本記事では、「 GitHub Actionsと AWS App Runnerを利用してBlue/Greenデプロイメントを実現してみた」というタイトルのとおり、 GitHub Actionsと AWS App Runner(以下、App Runner)、それに加えて、 Amazon CloudFront (以下、CloudFront)と Amazon Route 53(以下、Route 53)を利用してBlue/Greenデプロイメントを実現する方法を、サンプルコードとともにご紹介します! Blue/Greenデプロイメントって何? App Runnerって何? 事前に準備すること App Runnerサービスを作成する CloudFrontディストリビューションを作成する Route 53のレコードを作成する GitHubのRepository secretsを設定する GitHub ActionsのワークフローのYAMLファイルを作成する 実際にBlue/Greenデプロイメントしてみる GitHub ActionsのワークフローのYAMLファイルの改善方法 GitHub Actionsのログ出力の制御 OpenID Connectを利用したAWSリソースへのアクセス CloudFrontディストリビューションのキャッシュパージ おわりに Blue/Greenデプロイメントって何? Blue/Greenデプロイメントとは、本番環境と検証環境を交互に入れ替えることにより、Webアプリケーションなどに変更を加えるデプロイ手法の一つです。ざっくりとした流れとしては、「本番環境であるBlue」と「本番環境とほぼ同一の検証環境Green」の2つを用意した上で、以下の1.〜4.を繰り返すことになります。 Greenに変更を加えていく 問題がなければGreenを本番環境に、Blueを検証環境にチェンジする(デプロイ) 今度はBlueに変更を加えていく 問題がなければ再びBlueを本番環境に、Greenを検証環境にチェンジする(デプロイ) メリットとしては、デプロイ時や ロールバック 時のシステムのダウンタイムを最小限にできることや、限りなく本番環境に近い検証環境でテストできることなどが挙げられます。 一方、デメリットとしては、 アーキテクチャ が複雑になることや、インフラ構築用のコードと実際のインフラ環境に矛盾が生じる(インフラ構築用のコード上でBlueを本番環境としていた場合、Greenが本番環境になっている間はインフラ構築用のコードと実際のインフラ環境が一致しない)ことなどが挙げられます。 App Runnerって何? App Runnerとは、コンテナベースの AWS リソースの一つです。噛み砕いて言えば、 Amazon ECS、 AWS Fargate、ELBといった種々のリソースを全部まとめて裏でイイ感じにやってくれるものです。非常に手軽で簡単にWebアプリケーションをデプロイできる反面、制約も多いです。詳しくは 公式ドキュメント を参照ください。 事前に準備すること 各種 AWS リソースや、Blue/Greenデプロイメントを実現するための GitHub Actionsのワークフローを作成します。最終的な アーキテクチャ は下図になります。 App Runnerサービスを作成する まずは、 公式ドキュメント の手順に従い、サンプルアプリケーションがデプロイされているApp Runnerサービスを2つ作成します。 Prerequisites の手順に従い、サンプル リポジトリ を作成します。 リポジトリ 名は任意でOKです。 ステップ 1: App Runner サービスを作成する の手順に従い、App Runnerサービスを作成します。 GitHub connectionsの接続名や 環境変数 NAME の値は任意でOKです。サービス名はそれぞれ sample-service-blue 、 sample-service-green とします。また、デプロイ設定の部分で、 sample-service-blue のデプロイトリガーを 手動 に、 sample-service-green のデプロイトリガーを 自動 に、それぞれ設定してください。 何故片方を手動デプロイ、もう片方を自動デプロイにするかというと、自動デプロイの場合、指定した リポジトリ 、ブランチの ソースコード に変更を加える(=pushする)度に、App Runnerサービスにも変更が反映される(=最新の ソースコード を基にアプリケーションがデプロイし直される)ためです。これにより、本番環境(手動デプロイ)に影響を与えることなく、検証環境(自動デプロイ)に変更を加えることが可能となります。 ソースコード をpushするだけで最新のアプリケーションが自動でデプロイされるのは非常に手軽で便利ですね! CloudFront ディストリビューション を作成する 次に、 公式ドキュメント の手順に従い、CloudFront ディストリビューション を2つ作成します。基本的にはデフォルト設定のままで問題ありませんが、以下の項目はデフォルトから変更をお願いします。 2つの ディストリビューション 共通の設定 項目 値 プロトコル HTTPSのみ 料金クラス 北米、欧州、アジア、中東、アフリカを使用 (Blue/Greenデプロイメントには直接関係ありませんが、コストを抑えるためです) カスタム SSL証明書 任意の証明書 (本記事では example.com とします) それぞれの ディストリビューション で異なる設定 項目 1つ目の ディストリビューション に設定する値 2つ目の ディストリビューション に設定する値 オリジン ドメイン sample-service-blue のデフォルト ドメイン ( https:// の部分は不要です) sample-service-green のデフォルト ドメイン ( https:// の部分は不要です) 代替 ドメイン 名 カスタム SSL証明書 に対応する任意の ドメイン 名 (本記事では sample.example.com とします) なし 説明 sample-distribution-blue sample-distribution-green 何故片方だけに代替 ドメイン 名を入力するかというと、CloudFrontのルールとして、複数の ディストリビューション に同一の代替 ドメイン を同時に設定できないようになっているためです。つまり、代替 ドメイン は、常にルーティング先が本番環境になっている方の ディストリビューション にのみ設定されるように、適宜付け替える必要があります。 Route 53のレコードを作成する 続いて、 公式ドキュメント の手順に従い、Route 53のレコードを2つ作成します。 1つ目(Aレコード) 項目 値 レコード名 sample-distribution-blue の代替 ドメイン 名と対応する名称 (本記事では sample とします) レコードタイプ A の エイリアス トラフィック のルーティング先 CloudFrontディストリビューションへのエイリアス > sample-distribution-blue の ドメイン 名 ルーティングポリシー シンプルルーティング 2つ目(TXTレコード) 項目 値 レコード名 Aレコードのレコード名の先頭に _ を付したもの (本記事では _sample とします) レコードタイプ TXT トラフィック のルーティング先 sample-distribution-green の ドメイン 名の末尾に . を付したもの ルーティングポリシー シンプルルーティング 何故2つ目のTXTレコードが必要になるかというと、 CloudFrontディストリビューションを作成する の節で説明した代替 ドメイン の付け替えに必要になるためです。このレコードに、代替 ドメイン を設定しない方の ディストリビューション の ドメイン を設定しておく必要があります。レコード名の先頭に _ を付けたり値の末尾に . を付けたりするのは仕様です。詳しくは 公式ドキュメント を参照ください。 GitHub のRepository secretsを設定する ここからは GitHub 側の準備になります。 GitHub Actionsの利用に際して、 AWS の認証情報や間接的に取得することが難しい値を、あらかじめRepository secretsに設定しておきます。 公式ドキュメント の手順に従い、以下の4つを設定してください。 Name Value AWS_ACCESS_KEY_ID 自身の AWS アクセスキーID AWS_SECRET_ACCESS_KEY 自身の AWS シークレットアクセスキー AWS_ROUTE53_HOSTED_ZONE_ID Route 53のホストゾーンID AWS_ROUTE53_RECORD_NAME sample.example.com (Route 53のAレコードのレコード名) GitHub Actionsのワークフローの YAML ファイルを作成する 最後に、実際にBlue/Greenデプロイメントを実現する部分である、 GitHub Actionsのワークフローの YAML ファイルを作成します。今回の構成でBlue/Greenデプロイメントの実現に必要な要素は、以下の4つです。 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する CloudFront ディストリビューション の代替 ドメイン 名を付け替える Route 53のAレコードとTXTレコードのルーティング先を入れ替える まずはこれらを、 AWS CLI のコマンドを用いて GitHub Actionsのワークフローに落とし込みます。なお、以下の例では、 release/〇〇 のようなタグのpushをBlue/Greenデプロイメントのトリガーとしています。 name : Blue/Green Deploy on : push : tags : - release/* jobs : deploy : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 # 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する - name : Change Source Configuration of Current Service run : aws apprunner update-service --service-arn `本番環境側のApp RunnerサービスのARN` --source-configuration `設定ファイルのパス` # 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する - name : Change Source Configuration of Next Service run : aws apprunner update-service --service-arn `検証環境側のApp RunnerサービスのARN` --source-configuration `設定ファイルのパス` # CloudFrontディストリビューションの代替ドメイン名を付け替える - name : Replace Alias run : aws cloudfront associate-alias --target-distribution-id `検証環境側のCloudFrontディストリビューションのID` --alias ${{ secrets.AWS_ROUTE53_RECORD_NAME }} # Route 53のAレコードとTXTレコードのルーティング先を入れ替える - name : Change Record Targets run : aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --change-batch `設定ファイルのパス` このワークフローを実行することにより、現在の検証環境が本番環境となり、Aレコードの ドメイン からアクセス可能になります。一方、現在の本番環境は検証環境となり、 ソースコード をpushするだけで最新のアプリケーションが自動でデプロイされます。 しかしながら、このままでは現在の本番環境がどちらかをその都度手動で調べたり、各リソースのARNやIDなどをコピペしたり、設定ファイルを自作したりする必要があります。当然、そんなことをしていては非常に面倒ですし、ヒューマンエラーも発生しやすくなってしまいます。 そのため、それらの処理も併せて自動化したものが以下になります。 name : Blue/Green Deploy on : push : tags : - release/* jobs : deploy : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 # CloudFrontディストリビューションの一覧を取得し、その中から説明が`sample-distribution`で始まっているものの情報をファイルに書き込む # 代替ドメインが1個のものを現在の本番環境、0個のものを検証環境と判定する - name : Get Distribution List run : | aws cloudfront list-distributions | jq '.DistributionList.Items[] | select(.Comment | startswith("sample-distribution"))' > distribution-list.json cat distribution-list.json | jq 'select(.Aliases.Quantity==1)' > current-distribution.json cat distribution-list.json | jq 'select(.Aliases.Quantity==0)' > next-distribution.json # 2つのCloudFrontディストリビューションのドメイン名を取得し、変数に代入する - name : Get Distribution Domains id : distribution-domains run : | echo "::set-output name=CURRENT_DISTRIBUTION_DOMAIN::$(cat current-distribution.json | jq -r '.DomainName')" echo "::set-output name=NEXT_DISTRIBUTION_DOMAIN::$(cat next-distribution.json | jq -r '.DomainName')" # 検証環境側のCloudFrontディストリビューションのIDを取得し、変数に代入する - name : Get Distribution ID id : distribution-id run : echo "::set-output name=NEXT_DISTRIBUTION_ID::$(cat next-distribution.json | jq -r '.Id')" # App Runnerサービスの一覧を取得し、ファイルに書き込む - name : Get Service List run : aws apprunner list-services | jq '.ServiceSummaryList[]' > service-list.json # 2つのApp Runnerサービスのドメインを取得し、変数に代入する - name : Get Service Domains id : service-domains run : | echo "::set-output name=CURRENT_SERVICE_DOMAIN::$(cat current-distribution.json | jq -r '.Origins.Items[0].DomainName')" echo "::set-output name=NEXT_SERVICE_DOMAIN::$(cat next-distribution.json | jq -r '.Origins.Items[0].DomainName')" # 2つのApp RunnerサービスのARNを取得し、変数に代入する - name : Get Service ARNs id : service-arns run : | echo "::set-output name=CURRENT_SERVICE_ARN::$(cat service-list.json | jq -r --arg domain ${{ steps.service-domains.outputs.CURRENT_SERVICE_DOMAIN }} 'select(.ServiceUrl==$domain) | .ServiceArn')" echo "::set-output name=NEXT_SERVICE_ARN::$(cat service-list.json | jq -r --arg domain ${{ steps.service-domains.outputs.NEXT_SERVICE_DOMAIN }} 'select(.ServiceUrl==$domain) | .ServiceArn')" # 2つのApp Runnerサービスの設定情報を取得し、デプロイトリガー部分を変更した上でファイルに書き込む - name : Get Service Config Files run : | aws apprunner describe-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} | jq '.Service.SourceConfiguration | .AutoDeploymentsEnabled=true' > current-service-config.json aws apprunner describe-service --service-arn ${{ steps.service-arns.outputs.NEXT_SERVICE_ARN }} | jq '.Service.SourceConfiguration | .AutoDeploymentsEnabled=false' > next-service-config.json # 指定したホストゾーンIDのRoute 53のレコードの一覧を取得し、指定したレコード名のものと、指定したレコード名の先頭に`_`を付したものの情報をファイルに書き込む # 2つのレコードの設定部分を変更した上でファイルに書き込む - name : Get Record Config File run : | aws route53 list-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --output json | jq --arg name ${{ secrets.AWS_ROUTE53_RECORD_NAME }} '.ResourceRecordSets[] | select(.Name | endswith($name+"."))' > record-list.json cat record-list.json | jq --arg domain ${{ steps.distribution-domains.outputs.CURRENT_DISTRIBUTION_DOMAIN }} 'select(.Type=="TXT") | .ResourceRecords[0].Value="\""+$domain+".\"" | {"Changes":[{"Action":"UPSERT"}+{ResourceRecordSet:.}]}' > txt-record.json cat record-list.json | jq --arg domain ${{ steps.distribution-domains.outputs.NEXT_DISTRIBUTION_DOMAIN }} 'select(.Type=="A") | .AliasTarget.DNSName=$domain+"." | {"Changes":[{"Action":"UPSERT"}+{ResourceRecordSet:.}]}' > a-record.json cat txt-record.json a-record.json | jq -s '.[0].Changes+.[1].Changes | {"Changes":.}' > record-config.json # 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する - name : Change Source Configuration of Current Service run : aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} --source-configuration file://current-service-config.json # 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する - name : Change Source Configuration of Next Service run : aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.NEXT_SERVICE_ARN }} --source-configuration file://next-service-config.json # CloudFrontディストリビューションの代替ドメイン名を付け替える - name : Replace Alias run : aws cloudfront associate-alias --target-distribution-id ${{ steps.distribution-id.outputs.NEXT_DISTRIBUTION_ID }} --alias ${{ secrets.AWS_ROUTE53_RECORD_NAME }} # Route 53のAレコードとTXTレコードのルーティング先を入れ替える - name : Change Record Targets run : aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --change-batch file://record-config.json # 作成したファイルを削除する - name : Delete Temporary Files if : ${{ always() }} run : rm distribution-list.json current-distribution.json next-distribution.json service-list.json current-service-config.json next-service-config.json record-list.json txt-record.json a-record.json record-config.json 実際にBlue/Greenデプロイメントしてみる それでは、実際にBlue/Greenデプロイメントのワークフローを動作させ、環境の切り替わりを確認してみます。 現在の本番環境の状態を確認するために、 sample.example.com にアクセスします。 サンプルアプリケーションの文字列がそのまま表示されます。 現在の検証環境の状態を確認するために、 sample-distribution-green の ドメイン にアクセスします。 本番環境と同一の画面が表示されます。 server.py の8行目の Hello を こんにちは に変更して、 GitHub リポジトリ にpushします。 MESSAGE = "こんにちは, " + name + "!" sample-service-green へのデプロイが完了するのを待ってから、再度 sample-distribution-green の ドメイン にアクセスしてみます。 こんにちは に更新されているため、検証環境には先ほどpushした内容が反映されていることが分かります。 再度 sample.example.com にアクセスしてみます。 Hello のままであるため、先ほどのpushが本番環境には影響を与えていないことが分かります。 release/v1.0.0 というタグをpushします。 GitHub Actionsのワークフローが正常に完了していれば成功です。 git tag release/v1.0.0 git push origin release/v1.0.0 再度 sample.example.com にアクセスしてみます。 こんにちは に更新されています。Blue/Greenデプロイメントにより、先ほどまでの検証環境が数十秒のうちに本番環境に切り替わったことが確認できました! なお、確認は省略しますが、今回確認したBlueからGreenへの切り替えだけでなく、GreenからBlueへの切り替えも正常に動作します。 GitHub Actionsのワークフローの YAML ファイルの改善方法 GitHub Actionsのログ出力の制御 本記事にサンプルとして掲載した YAML ファイルによるワークフローを実行すると、Repository secrets以外の変数の値や AWS CLI のコマンドの実行結果が GitHub Actionsのログに出力されてしまいます。そのため、Publicな リポジトリ で実行する場合には、 ログ中での値のマスク や nul へのリダイレクトなどを利用して、ログの出力を工夫する必要があります。 # 値のマスクの例 CURRENT_DISTRIBUTION_DOMAIN=$(cat current-distribution.json | jq -r '.DomainName') echo "::add-mask::$CURRENT_DISTRIBUTION_DOMAIN" echo "::set-output name=CURRENT_DISTRIBUTION_DOMAIN::$CURRENT_DISTRIBUTION_DOMAIN" # nulへのリダイレクトの例 aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} --source-configuration file://current-service-config.json > nul OpenID Connectを利用した AWS リソースへのアクセス GitHub Actionsから AWS リソースにアクセスする方法に関しては、 OpenID Connectを利用する方法が先日発表されました。こちらの方法を利用すると、 AWS アクセスキーIDや AWS シークレットアクセスキーを利用せずに AWS リソースにアクセスできるため、より安全性を高められます。詳しくは、 公式ドキュメント や、テックブログ記事である OpenID Connectを利用してGitHub ActionsからAWSリソースにアクセスする を参照ください。 CloudFront ディストリビューション のキャッシュパージ Blue/Greenデプロイメントのワークフローの YAML ファイルとは別に、 main ブランチへのpushをトリガーとして検証環境側のCloudFront ディストリビューション のキャッシュをパージするワークフローの YAML ファイルを作成することにより、アプリケーションをデプロイし直す際にキャッシュによって古い情報が配信されることを防げます。 # キャッシュパージの例 NEXT_DISTRIBUTION_ID=$(aws cloudfront list-distributions | jq -r '.DistributionList.Items[] | select(.Comment | startswith("sample-distribution")) | select(.Aliases.Quantity==0) | .Id') aws cloudfront create-invalidation --distribution-id $NEXT_DISTRIBUTION_ID --paths "/*" おわりに 本記事では、 GitHub Actions、App Runner、CloudFront、Route 53を利用してBlue/Greenデプロイメントを実現する方法をご紹介しました。 GitHub リポジトリ に ソースコード をpushするだけで検証環境にアプリケーションがデプロイされ、タグをpushするだけでBlue/Greenデプロイメントが完了するというのは、開発活動を進めていく上で非常に便利で快適です。機会があれば皆さんもぜひお試しください! 電通国際情報サービス Advent Calendar 2021 3日目となる明日の記事は 比嘉康雄 さんの「 Geth(ゲス)はじめました 」です!お楽しみに! 最後までお読みいただき、本当にありがとうございました! 私たちは同じ事業部で共に働いていただける仲間を募集しています! みなさまのご応募、お待ちしています! フルサイクルエンジニア 執筆: @miyazawa.hibiki 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
これは 電通国際情報サービス アドベントカレンダー の一日目の記事です。 はじめに 使っているツール Teams Shodo はてなブログ Markdownで書けること 高速に動作すること 見た目の調整にCSSやHTMLを使えること 記事を簡単にエクスポートできること まとめ はじめに みなさんこんにちは、 電通国際情報サービス (ISID)X イノベーション 本部アドバンストテク ノロ ジー 部の佐藤太一です。 ISIDの技術的な取組みを外部に公開することで、ISIDをより多くのみなさんに知ってもらうためテックブログを始めました。 アドベントカレンダー の時期は、たくさんのエントリが公開されるので皆さんお忙しいとは思いますが、毎日新しい記事を公開予定ですので是非ごらんになってください。 使っているツール まずは、このブログを執筆するにあたって利用しているサービスやツールを紹介します。 Teams ISIDでは標準的なチャットツールとして、 Microsoft Teamsを使っています。 そこにテックブログ用のチームを作成して、参加メンバーのサポートや事務連絡を行っています。 Shodo Shodoはこういったブログエントリのような文章を執筆するために必要な機能が全部入った便利な執筆管理 SaaS です。 アイディアのメモ書きとタスク管理と執筆用の Markdown エディター、それに加えてレビュー管理ができます。 普段の仕事でやっているように GitHub 上でtextlintを使ったCIを回しながらブログを投稿する方法も検討しましたが、仕組みが複雑になりすぎるので諦めました。 はてなブログ ブログサービスはたくさんありますが、 はてなブログ を採用した理由を説明します。 Markdown で書けること Markdown 記法を使って記事を書けることは個人的にかなり重要視しています。 テックブログですから何かを比較検討することは多いと考えていますので、データをきれいに並べて見せられる テーブル記法 は非常に大切です。 また、 ソースコード をエントリの中に綺麗に記述したいのでコード記法が適切に動作することも必要です。 加えて、 # で見出しを並べておくと [:contents] で目次が簡単に作れるのも気に入っています。 高速に動作すること ブログのようなメディアはどんなに遅くとも3000ms以内にはコンテンツが読める状態で表示されていることが望ましいと考えています。いわゆる 三秒ルール ですね。 これは、読者が読みたいという気持ちのあるコンテンツであれば待ってもらえます。しかし、待ってもらえるということはコンテンツの質に対する期待が高まります。テックブログのように軽量なメディアでは、恐らくその期待を満たすのは難しいでしょう。 ウェブに関連する技術は極めて高速に変化している上に多様です。サーバだけでなく、経路のネットワーク、クライアントであるブラウザ、またブラウザの実行環境はパソコンだけでなく スマートフォン もあります。つまり、読者のコンテンツを届ける経路には多くの要素技術が濃縮されているのです。 私たちがコンテンツのホストを SaaS にお願いするのは、こういった最適化を主たる業務として実施しているサービス提供者の方が上手くやれるからです。 見た目の調整に CSS やHTMLを使えること 自社メディアの一種としてブログを運営するのですから、 ブランディング は大切なことです。 私たちが作成したコンテンツはホストしていただいているサービスに譲渡するわけではありません。また、ホストしているサービスが提供するコンテンツの一部というわけでもありません。 どれだけ簡単に使えるとしても、ビジュアルデザインがホストしているサービスの一部になってしまうようでは、企業としての ブランディング を損なうと私は考えています。 今の公開状態では私が見た目を調節しているので、それほど高度なビジュアルデザインは実施していません。それでも、ロゴやヘッダー画像、背景色などはコーポレートサイトとできるかぎり一致させています。 記事を簡単にエクスポートできること これも ブランディング の話の続きです。 私たちのメディアに対する姿勢が変われば SaaS の選定基準は当然変わります。その変化によってコンテンツを預けるサービスが変わることはあるでしょう。そういった時、それまでに作ったコンテンツを移行するのが難しいサービスを使いたくはありません。 もちろん、ウェブですからHTMLを スクレイピング すればあらゆるコンテンツは移行可能ですが、そういう強引なやり方をせずにコンテンツ移行できることが望ましいと私は考えます。 はてなブログ では AtomPub に対応しています。加えて、 MovableType 形式のテキストファイルによるエクスポートもあります。 まとめ この記事では主にテックブログのコンテンツをホストするためのサービスとして はてなブログ を採用した理由を説明しました。 Shodoは便利に使っているのですが、まだ使い始めたばかりで勝手が分からない部分もあり、今回は軽い説明になりました。 ISIDの アドベントカレンダー は今日始まったばかりですが、このまま25日まで続きますので、皆さんお楽しみに! 執筆: @sato.taichi 、レビュー: @handa.kenta ( Shodo で執筆されました )
アバター
これは 電通国際情報サービス アドベントカレンダー の一日目の記事です。 はじめに 使っているツール Teams Shodo はてなブログ Markdownで書けること 高速に動作すること 見た目の調整にCSSやHTMLを使えること 記事を簡単にエクスポートできること まとめ はじめに みなさんこんにちは、 電通国際情報サービス (ISID)X イノベーション 本部アドバンストテク ノロ ジー 部の佐藤太一です。 ISIDの技術的な取組みを外部に公開することで、ISIDをより多くのみなさんに知ってもらうためテックブログを始めました。 アドベントカレンダー の時期は、たくさんのエントリが公開されるので皆さんお忙しいとは思いますが、毎日新しい記事を公開予定ですので是非ごらんになってください。 使っているツール まずは、このブログを執筆するにあたって利用しているサービスやツールを紹介します。 Teams ISIDでは標準的なチャットツールとして、 Microsoft Teamsを使っています。 そこにテックブログ用のチームを作成して、参加メンバーのサポートや事務連絡を行っています。 Shodo Shodoはこういったブログエントリのような文章を執筆するために必要な機能が全部入った便利な執筆管理 SaaS です。 アイディアのメモ書きとタスク管理と執筆用の Markdown エディター、それに加えてレビュー管理ができます。 普段の仕事でやっているように GitHub 上でtextlintを使ったCIを回しながらブログを投稿する方法も検討しましたが、仕組みが複雑になりすぎるので諦めました。 はてなブログ ブログサービスはたくさんありますが、 はてなブログ を採用した理由を説明します。 Markdown で書けること Markdown 記法を使って記事を書けることは個人的にかなり重要視しています。 テックブログですから何かを比較検討することは多いと考えていますので、データをきれいに並べて見せられる テーブル記法 は非常に大切です。 また、 ソースコード をエントリの中に綺麗に記述したいのでコード記法が適切に動作することも必要です。 加えて、 # で見出しを並べておくと [:contents] で目次が簡単に作れるのも気に入っています。 高速に動作すること ブログのようなメディアはどんなに遅くとも3000ms以内にはコンテンツが読める状態で表示されていることが望ましいと考えています。いわゆる 三秒ルール ですね。 これは、読者が読みたいという気持ちのあるコンテンツであれば待ってもらえます。しかし、待ってもらえるということはコンテンツの質に対する期待が高まります。テックブログのように軽量なメディアでは、恐らくその期待を満たすのは難しいでしょう。 ウェブに関連する技術は極めて高速に変化している上に多様です。サーバだけでなく、経路のネットワーク、クライアントであるブラウザ、またブラウザの実行環境はパソコンだけでなく スマートフォン もあります。つまり、読者のコンテンツを届ける経路には多くの要素技術が濃縮されているのです。 私たちがコンテンツのホストを SaaS にお願いするのは、こういった最適化を主たる業務として実施しているサービス提供者の方が上手くやれるからです。 見た目の調整に CSS やHTMLを使えること 自社メディアの一種としてブログを運営するのですから、 ブランディング は大切なことです。 私たちが作成したコンテンツはホストしていただいているサービスに譲渡するわけではありません。また、ホストしているサービスが提供するコンテンツの一部というわけでもありません。 どれだけ簡単に使えるとしても、ビジュアルデザインがホストしているサービスの一部になってしまうようでは、企業としての ブランディング を損なうと私は考えています。 今の公開状態では私が見た目を調節しているので、それほど高度なビジュアルデザインは実施していません。それでも、ロゴやヘッダー画像、背景色などはコーポレートサイトとできるかぎり一致させています。 記事を簡単にエクスポートできること これも ブランディング の話の続きです。 私たちのメディアに対する姿勢が変われば SaaS の選定基準は当然変わります。その変化によってコンテンツを預けるサービスが変わることはあるでしょう。そういった時、それまでに作ったコンテンツを移行するのが難しいサービスを使いたくはありません。 もちろん、ウェブですからHTMLを スクレイピング すればあらゆるコンテンツは移行可能ですが、そういう強引なやり方をせずにコンテンツ移行できることが望ましいと私は考えます。 はてなブログ では AtomPub に対応しています。加えて、 MovableType 形式のテキストファイルによるエクスポートもあります。 まとめ この記事では主にテックブログのコンテンツをホストするためのサービスとして はてなブログ を採用した理由を説明しました。 Shodoは便利に使っているのですが、まだ使い始めたばかりで勝手が分からない部分もあり、今回は軽い説明になりました。 ISIDの アドベントカレンダー は今日始まったばかりですが、このまま25日まで続きますので、皆さんお楽しみに! 執筆: @sato.taichi 、レビュー: @handa.kenta ( Shodo で執筆されました )
アバター
皆さん、こんにちは。 電通国際情報サービス (以下、ISID)X イノベーション 本部アドバンストテク ノロ ジー 部の宮原です。 本日よりISIDのテックブログが開始されます。そして、本記事はISIDテックブログの記念すべき第一号です。 また、12月からはAdvent Calendarを実施していく予定です。Advent Calendarのリンクは以下にあります。 https://adventar.org/calendars/6576 こちらもぜひ楽しみにしていてください。 さて、第一号の本記事では GitHub Actionsの新機能についてご紹介します。 10/27に GitHub Blogにて以下のアナウンスがありました。 https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/ ブログのタイトルは GitHub Actions: Secure cloud deployments with OpenID Connectとなっています。このアナウンスによって、 OpenID Connectを利用し GitHub Actionsから クラウド リソースにアクセスできるようになりました。 本記事では OpenID Connectを利用し、 GitHub Actionsから AWS リソースへアクセスする方法についてご紹介します。 以下では OpenID ConnectをOIDCと略します。 概要 方法 IAM OIDC ID プロバイダーを作成する IAM ロールを作成する GitHub Actionsから一時的な認証情報を取得する まとめ 参考にした情報 概要 OIDCを利用した クラウド リソースへのアクセスについては以下のドキュメントを参考にすると良いでしょう。 https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect 以下は公式ドキュメントの画像です。 クラウド プロバイダーにてロールと GitHub Actionsジョブ間のOIDCの信頼関係を設定する。 GitHub Actionsのジョブが実行されるたび、 GitHub のOIDCプロバイダーはOIDC トーク ンを自動生成する。 GitHub のOIDCプロバイダーからOIDC トーク ンを取得し、 クラウド プロバイダーに送信する。 クラウド プロバイダーはOIDC トーク ンの検証に成功したら、ジョブ内でのみ有効な、一時的な認証 トーク ンを提供する。 このような流れで一時的な認証 トーク ンを入手し、さらに認証 トーク ンを用いて クラウド リソースにアクセスします。 上記の流れを実現するために、 AWS では、 Web IDフェデレーション の機能を利用します。Web IDフェデレーションを利用し、最終的にはIAMロールを マッピング した一時的な認証 トーク ンを提供します。 方法 ここからは、OIDCを利用し GitHub Actionsから AWS リソースへアクセスする方法について記述します。 今回は、S3へのファイルアップロードを行う GitHub Actionsジョブを作成します。 AWS の各設定は全て GUI から行いました。 IAM OIDC ID プロバイダーを作成する まずは、 AWS 上でOIDC IDプロバイダーを作成します。これは GitHub OIDCプロバイダーの情報を AWS に登録する作業です。 AWS コンソールのIAM、IDプロバイダーの作成からIDプロバイダーを作成します。 OIDC IDプロバイダーの設定情報は以下のドキュメントに載っています。 https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services ドキュメントに則り、OIDC IDプロバイダーの設定情報として以下を入力しました。 プロバイダーのURL: https://token.actions.githubusercontent.com 対象者: sts .amazonaws.com aws-actions/configure-aws-credentials を利用する場合は対象者の項目に sts .amazonaws.comを入力します。 IAM ロールを作成する 次にIAM ロールを作成します。コンソールからIAM、ロール、ロールを作成を選択し、ロールを作成します。 今回はWeb ID フェデレーションの機能を利用するため、信頼されたエンティティの種類にウェブIDを選択します。 IDプロバイダー、Audienceには先ほど作成した、IDプロバイダーの情報を入力します。 アクセス権限はGitHubActionsで実施したい作業に応じて設定します。 今回はS3へのファイルアップロードを実施するため、特定の バケット へのPutObjectを許可するポリシーを持つ、IAMロールを作成しました。 IAMロールを作成した後に信頼関係の修正を行います。特定の GitHub リポジトリ からのみのアクセスに制限する設定を追加します。 この作業は必ず実施してください。 制限を追加しないと、全ての リポジトリ の GitHub Actions上からIAMを利用できてしまいます。 信頼関係を修正するために、作成したIAMロールを選択し、信頼関係のタブから信頼関係の編集を選択します。 エディターが開かれるので、項目を追加します。今回はStatement.Conditionの項目に以下の条件を追加します。 これによって特定の リポジトリ からのみ、IAMを利用できます。 * の部分を詳細に記述し、ブランチの制限を加えることも可能です。 " StringLike ": { " token.actions.githubusercontent.com:sub ": " repo:<リポジトリの所有者>/<リポジトリ名>:* " } 最終的な信頼関係のポリシーは以下になります。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Principal ": { " Federated ": " arn:aws:iam::<AWSのアカウントID>:oidc-provider/token.actions.githubusercontent.com " } , " Action ": " sts:AssumeRoleWithWebIdentity ", " Condition ": { " StringEquals ": { " token.actions.githubusercontent.com:aud ": " sts.amazonaws.com " } , " StringLike ": { " token.actions.githubusercontent.com:sub ": " repo:<リポジトリの所有者>/<リポジトリ名>:* " } } } ] } aws -actions/configure- aws -credentialsに IAMロール作成のCloudFormation Template があります。こちらを利用してIAMロールを作成するのも良いでしょう。 GitHub Actionsから一時的な認証情報を取得する 最後に GitHub Actionsの設定です。 GitHub Actionsの YAML ファイルを記述します。今回は以下の YAML ファイルを作成しました。 name : Upload File on : push jobs : upload : runs-on : ubuntu-latest permissions : id-token : write contents : read steps : - uses : actions/checkout@v2 - uses : aws-actions/configure-aws-credentials@master with : role-to-assume : ${{ secrets.AWS_IAM_ROLE_ARN }} role-session-name : github-actions-sample-session aws-region : ap-northeast-1 - run : aws s3 cp ./test.txt s3://${{ secrets.AWS_S3_BUCKET_NAME }}/ GitHub Actionsのジョブ内でOIDC トーク ンを取得するにはpermissionsのid-tokenの権限をwriteにする必要があります。 IAMロールのARNは リテラル で書くこともできますが、できる限り GitHub ActionsのSecretに記述したほうが良いでしょう。 IAMロールのARNが漏えいし、先程の信頼関係が適切に設定されていないと、不正利用されてしまいます。 また、利用する aws -actions/configure- aws -credentialsのバージョンをv1ではなく、masterにしています。v1のリリースにはOIDCの機能が含まれていないため、masterブランチを指定しないと動作しません。こちらも注意するポイントです。 この設定で、特定のS3 バケット へのファイルアップロードが可能になりました。 まとめ 本記事では GitHub ActionsのOIDC機能を利用し、 GitHub Actionsから AWS リソースへアクセスする方法について記述しました。OIDCを利用することによって、アクセスキーを利用せず AWS リソースにアクセスできるようになりました。 これによってアクセスキーの漏えいのリスクを軽減できます。しかしながら、IAMロールの信頼関係を適切に設定しないと、別の不正利用にも繋がります。慎重に設定しましょう。 最後までお読みいただきありがとうございました。ISID Advent Calendarも楽しみにしていください。 参考にした情報 https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/ https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services https://github.com/aws-actions/configure-aws-credentials/blob/master/README.md 執筆: @miyahara.hikaru 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター
皆さん、こんにちは。 電通国際情報サービス (以下、ISID)X イノベーション 本部アドバンストテク ノロ ジー 部の宮原です。 本日よりISIDのテックブログが開始されます。そして、本記事はISIDテックブログの記念すべき第一号です。 また、12月からはAdvent Calendarを実施していく予定です。Advent Calendarのリンクは以下にあります。 https://adventar.org/calendars/6576 こちらもぜひ楽しみにしていてください。 さて、第一号の本記事では GitHub Actionsの新機能についてご紹介します。 10/27に GitHub Blogにて以下のアナウンスがありました。 https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/ ブログのタイトルは GitHub Actions: Secure cloud deployments with OpenID Connectとなっています。このアナウンスによって、 OpenID Connectを利用し GitHub Actionsから クラウド リソースにアクセスできるようになりました。 本記事では OpenID Connectを利用し、 GitHub Actionsから AWS リソースへアクセスする方法についてご紹介します。 以下では OpenID ConnectをOIDCと略します。 概要 方法 IAM OIDC ID プロバイダーを作成する IAM ロールを作成する GitHub Actionsから一時的な認証情報を取得する まとめ 参考にした情報 概要 OIDCを利用した クラウド リソースへのアクセスについては以下のドキュメントを参考にすると良いでしょう。 https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect 以下は公式ドキュメントの画像です。 クラウド プロバイダーにてロールと GitHub Actionsジョブ間のOIDCの信頼関係を設定する。 GitHub Actionsのジョブが実行されるたび、 GitHub のOIDCプロバイダーはOIDC トーク ンを自動生成する。 GitHub のOIDCプロバイダーからOIDC トーク ンを取得し、 クラウド プロバイダーに送信する。 クラウド プロバイダーはOIDC トーク ンの検証に成功したら、ジョブ内でのみ有効な、一時的な認証 トーク ンを提供する。 このような流れで一時的な認証 トーク ンを入手し、さらに認証 トーク ンを用いて クラウド リソースにアクセスします。 上記の流れを実現するために、 AWS では、 Web IDフェデレーション の機能を利用します。Web IDフェデレーションを利用し、最終的にはIAMロールを マッピング した一時的な認証 トーク ンを提供します。 方法 ここからは、OIDCを利用し GitHub Actionsから AWS リソースへアクセスする方法について記述します。 今回は、S3へのファイルアップロードを行う GitHub Actionsジョブを作成します。 AWS の各設定は全て GUI から行いました。 IAM OIDC ID プロバイダーを作成する まずは、 AWS 上でOIDC IDプロバイダーを作成します。これは GitHub OIDCプロバイダーの情報を AWS に登録する作業です。 AWS コンソールのIAM、IDプロバイダーの作成からIDプロバイダーを作成します。 OIDC IDプロバイダーの設定情報は以下のドキュメントに載っています。 https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services ドキュメントに則り、OIDC IDプロバイダーの設定情報として以下を入力しました。 プロバイダーのURL: https://token.actions.githubusercontent.com 対象者: sts .amazonaws.com aws-actions/configure-aws-credentials を利用する場合は対象者の項目に sts .amazonaws.comを入力します。 IAM ロールを作成する 次にIAM ロールを作成します。コンソールからIAM、ロール、ロールを作成を選択し、ロールを作成します。 今回はWeb ID フェデレーションの機能を利用するため、信頼されたエンティティの種類にウェブIDを選択します。 IDプロバイダー、Audienceには先ほど作成した、IDプロバイダーの情報を入力します。 アクセス権限はGitHubActionsで実施したい作業に応じて設定します。 今回はS3へのファイルアップロードを実施するため、特定の バケット へのPutObjectを許可するポリシーを持つ、IAMロールを作成しました。 IAMロールを作成した後に信頼関係の修正を行います。特定の GitHub リポジトリ からのみのアクセスに制限する設定を追加します。 この作業は必ず実施してください。 制限を追加しないと、全ての リポジトリ の GitHub Actions上からIAMを利用できてしまいます。 信頼関係を修正するために、作成したIAMロールを選択し、信頼関係のタブから信頼関係の編集を選択します。 エディターが開かれるので、項目を追加します。今回はStatement.Conditionの項目に以下の条件を追加します。 これによって特定の リポジトリ からのみ、IAMを利用できます。 * の部分を詳細に記述し、ブランチの制限を加えることも可能です。 " StringLike ": { " token.actions.githubusercontent.com:sub ": " repo:<リポジトリの所有者>/<リポジトリ名>:* " } 最終的な信頼関係のポリシーは以下になります。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Principal ": { " Federated ": " arn:aws:iam::<AWSのアカウントID>:oidc-provider/token.actions.githubusercontent.com " } , " Action ": " sts:AssumeRoleWithWebIdentity ", " Condition ": { " StringEquals ": { " token.actions.githubusercontent.com:aud ": " sts.amazonaws.com " } , " StringLike ": { " token.actions.githubusercontent.com:sub ": " repo:<リポジトリの所有者>/<リポジトリ名>:* " } } } ] } aws -actions/configure- aws -credentialsに IAMロール作成のCloudFormation Template があります。こちらを利用してIAMロールを作成するのも良いでしょう。 GitHub Actionsから一時的な認証情報を取得する 最後に GitHub Actionsの設定です。 GitHub Actionsの YAML ファイルを記述します。今回は以下の YAML ファイルを作成しました。 name : Upload File on : push jobs : upload : runs-on : ubuntu-latest permissions : id-token : write contents : read steps : - uses : actions/checkout@v2 - uses : aws-actions/configure-aws-credentials@master with : role-to-assume : ${{ secrets.AWS_IAM_ROLE_ARN }} role-session-name : github-actions-sample-session aws-region : ap-northeast-1 - run : aws s3 cp ./test.txt s3://${{ secrets.AWS_S3_BUCKET_NAME }}/ GitHub Actionsのジョブ内でOIDC トーク ンを取得するにはpermissionsのid-tokenの権限をwriteにする必要があります。 IAMロールのARNは リテラル で書くこともできますが、できる限り GitHub ActionsのSecretに記述したほうが良いでしょう。 IAMロールのARNが漏えいし、先程の信頼関係が適切に設定されていないと、不正利用されてしまいます。 また、利用する aws -actions/configure- aws -credentialsのバージョンをv1ではなく、masterにしています。v1のリリースにはOIDCの機能が含まれていないため、masterブランチを指定しないと動作しません。こちらも注意するポイントです。 この設定で、特定のS3 バケット へのファイルアップロードが可能になりました。 まとめ 本記事では GitHub ActionsのOIDC機能を利用し、 GitHub Actionsから AWS リソースへアクセスする方法について記述しました。OIDCを利用することによって、アクセスキーを利用せず AWS リソースにアクセスできるようになりました。 これによってアクセスキーの漏えいのリスクを軽減できます。しかしながら、IAMロールの信頼関係を適切に設定しないと、別の不正利用にも繋がります。慎重に設定しましょう。 最後までお読みいただきありがとうございました。ISID Advent Calendarも楽しみにしていください。 参考にした情報 https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/ https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services https://github.com/aws-actions/configure-aws-credentials/blob/master/README.md 執筆: @miyahara.hikaru 、レビュー: @sato.taichi ( Shodo で執筆されました )
アバター