電通総研のブログ - TECH PLAY

TECH PLAY

電通総研

電通総研 の技術ブログ

全867件

本記事は 電通国際情報サービス Advent Calendar 2021 の 13 日目の記事です。 執筆者は 2021 年新卒入社の XI 本部 AI トランスフォーメンションセンター所属の山田です。 はじめに React Hooks とは useState useState を使うユースケース useEffect useEffect を使うユースケース useContext useContext を使うユースケース useReducer useReducer を使うユースケース useMemo useMemo を使うユースケース useCallback React.memo と useCallback useCallback を使うユースケース React Hooks を正しく使うために おわりに はじめに 本記事では React Hooks の代表的なフックについて、その使い方と ユースケース をサンプルコードとともに紹介します。 サンプルコードは TypeScript で記述しています。 React Hooks とは React Hooks は React 16.8(2019 年 2 月リリース)で追加された機能です。 2021 年現在において React でアプリケーションを構築するためには理解が必須の機能といっても過言ではないでしょう。 React Hooks を使うことによって React の関数型 コンポーネント で状態(state)を持つことや コンポーネント のライフサイクルに応じた処理を記述できます。 以下では、 React で提供される基本的な React Hooks をその ユースケース とともに紹介します。 useState useEffect useContext useReducer useMemo useCallback useState useState は関数型 コンポーネント で状態(state)を扱うためのフックです。 以下は useState を利用する場合の基本的なコードです。 // 返り値はstateの変数とstateを更新するための関数 const [ state , setState ] = useState < T >( initStateValue ); useState は状態(state)の変数と状態(state)を更新するための関数を返します。 状態(state)の更新をする際は必ず更新用の関数を介して行う必要があります。 useState を使う ユースケース useState が必要となるのは、利用者と インタラクティブ にやり取りをする値を保持する必要がある場合です。 利用者と インタラクティブ にやり取りをするという場面の最も典型的な例はフォームです。 ここではログインフォームを題材にしてコードを紹介します。 作成するログインフォームは画像のように input 要素としてユーザー ID とパスワードを持つものを想定します。 初めにログインフォームで扱うデータの型(SampleLoginForm)を定義しておきます。 今回の例では userId と password だけをプロパティに持つオブジェクトとします。 interface SampleLoginForm { userId: string ; password: string ; } 作った SampleLoginForm 型の変数 formData を useState を使って定義します。 const [ formData , setFormData ] = useState < SampleLoginForm >( { userId: "" , password: "" , } ); あとは input 要素の value 属性に対応する formData の変数を渡します。 さらに input 要素の onChange イベントから setFormData を呼び出して formData の状態を更新します。 < div > < label htmlFor = "userId" > ユーザーID < /label > < input id = "userId" type= "text" name = "userId" placeholder = "ユーザーID" value = { formData.userId } onChange = { ( e ) => setFormData ( { ...formData , userId: e.target.value } ) } / > < /div > これにより、ユーザーがフォームに入力した文字(値)を変数 formData に保持できます。 ▶︎ クリックしてコード全文を見る // components/LoginForm.tsx import React , { useState } from "react" ; interface SampleLoginForm { userId: string ; password: string ; } export default function LoginForm () : JSX. Element { const [ formData , setFormData ] = useState < SampleLoginForm >( { userId: "" , password: "" , } ); const submitHandler = ( e: FormEvent < HTMLFormElement >) => { e.preventDefault (); console .log ( "ログインボタン押下" , formData ); } ; return ( < form onSubmit = { submitHandler } > < div > < label htmlFor = "userId" > ユーザーID < /label > < input id = "userId" type= "text" name = "userId" placeholder = "ユーザーID" value = { formData.userId } onChange = { ( e ) => setFormData ( { ...formData , userId: e.target.value } ) } / > < /div > < div > < label htmlFor = "password" > パスワード < /label > < input id = "password" type= "password" name = "password" placeholder = "パスワード" value = { formData.password } onChange = { ( e ) => setFormData ( { ...formData , password: e.target.value } ) } / > < /div > < div > < button type= "submit" > ログイン < /button > < /div > < /form > ); } useEffect useEffect は関数型 コンポーネント で副作用を実行するためのフックです。 副作用と聞くと仰々しいですが コンポーネント 内での「外部データの取得」「DOM の手動での更新」などの処理を、React では副作用と呼びます。 useEffect を使うための基本的なコードは以下のとおりです。 // 副作用を含む処理を記述した関数を記述する useEffect (() => { // 副作用処理 // … return () => { // クリーンアップ処理 } ; } , [] ); useEffect では副作用となる処理を関数内で記述します。 return で関数を返すことによってクリーンアップ処理を記述できます。 通常、 useEffect による副作用処理は コンポーネント の レンダリング 毎に実行されます。 副作用処理を毎回行わないためには、第2引数の依存配列によって制御できます。 useEffect の詳しい説明については以下の参考リンクをご覧ください。 副作用フックの利用法 https://ja.reactjs.org/docs/hooks-effect.html useEffect完全ガイド https://overreacted.io/ja/a-complete-guide-to-useeffect/ useEffect を使う ユースケース useEffect が必要となる代表的な ユースケース としては コンポーネント を呼び出したタイミングで外部 API からリソースを取得したい場合などです。 ここではサンプルの外部 API として JSONPlaceholder を利用して コンポーネント を呼び出したタイミングでデータを取得してみましょう。 JSONPlaceholder, https://jsonplaceholder.typicode.com/ 少し JSONPlaceholder について補足します。 JSONPlaceholder は 6 種類の構造のダミーデータを取得できます。 今回はタスク管理アプリケーションで一般的な ToDo リスト形式のデータを取得します。 取得したデータは先ほど紹介した setState を使って保持します。 まず取得する ToDo リストの型を定義しておきます。 interface ToDo { id: number ; userId: number ; title: string ; completed: boolean ; } そして先ほどの useState フックを使って取得する ToDo リスト形式を状態管理します。 const [ todoItemss , setToDos ] = useState < ToDo [] >( [] ); そして useEffect を使って実際に外部 API を呼び出し、状態を更新します。 外部 API の呼び出しには fetch を利用します。 useEffect (() => { const f = async () => { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); setToDos ( json ); } ; f (); } , [] ); 注意点として useEffect に渡す関数は同期的です。 そのため非同期関数(async/await)を使うには関数内で定義する必要があります。 補足ですが、次期アップデートの React v18 より React.Suspense を使った非同期のデータ取得がサポートされます。 アップデート後はこちらがベストプ ラク ティスになっていく可能性も高いため、公式ドキュメントの「React.Suspense」と「サスペンスを使ったデータ取得」についても、ぜひチェックをしてみてください。 React の最上位 API - React.Suspense, https://ja.reactjs.org/docs/react-api.html#reactsuspense サスペンスを使ったデータ取得(実験的機能), https://ja.reactjs.org/docs/concurrent-mode-suspense.html 取得した ToDo リスト形式のデータはスタイルを少し当てて Array.prototype.map() を使えば以下のように描画できます。 ▶︎ クリックしてコード全文を見る // components/ToDoList.tsx import React , { useEffect , useState } from "react" ; interface ToDo { id: number ; userId: number ; title: string ; completed: boolean ; } export default function ToDoList () : JSX. Element { const [ todoItems , setToDos ] = useState < ToDo [] >( [] ); useEffect (() => { const f = async () => { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); setToDos ( json ); } ; f (); } , [] ); return ( < div style = {{ textAlign: "left" }} > { todoItems.map (( todoItem ) => ( < div key = { todoItem.id } style = {{ width: "250px" , border: "solid" , margin: "8px" , padding: "8px" , }} > < h4 > { todoItem.title } < /h4 > < p style = {{ textAlign: "right" }} > { todoItem.completed ? "✅ 完了" : "未実施" } < /p > < /div > )) } < /div > ); } useContext useContext は コンポーネント 間で横断的に利用したい状態を管理するためのフックです。 通常、 コンポーネント では状態(データ)を props を通して親から子に渡します。 これを図に起こすと以下のようになります。 一方、コンテキストを使うと以下のように props を通さずにデータをやり取りできます。 コンテキストでは Context.Provider コンポーネント を通して横断的に利用したい状態を配信します。 そして必要な コンポーネント で useContext を使うことによって状態を購読します。 useContext を使って コンポーネント 内でコンテキストから配信される値を購読する基本的なコードは以下のようになります。 // 返り値はコンテキストから配信される値 // useContextの第1引数には`React.createContext`によって作成したコンテキストオブジェクトを渡す const value = useContext ( MyContext ); useContext では購読するコンテキストのオブジェクトを渡し、コンテキストから配信される値を受け取ります。 useContext を使う ユースケース ここまでで述べてきたように useContext を使うのは コンポーネント 間で横断的に利用したい状態がある場面です。 代表的な場面として認証情報の管理などがあります。 ここではコンテキストを使ってユーザー ID を管理することを例に説明します。 管理するユーザー ID は useState を用いて宣言し、その状態と更新用の関数をコンテキストを使って配信します。 // コンテキストで配信する値 const [ userId , setUserId ] = useState < number >( -1 ); 配信する値が決まったので、コンテキストで配信する値の型を定義します。 interface Context { userId: number ; setUserId: Dispatch < SetStateAction < number >>; } createContext を使ってコンテキストオブジェクトを作成します。型引数には先ほど定義した型を指定し、第 1 引数には初期値を与えます。 const AuthContext = createContext < Context >( { userId: -1 , setUserId: () => {} , } ); 次に Context の Provider を作成します。 Provider の value プロパティにコンテキストで配信する値を指定します。 const AuthProvider: React.FC = ( { children } ) => { // コンテキストで配信する値 const [ userId , setUserId ] = useState < number >( -1 ); return ( < AuthContext.Provider value = {{ userId , setUserId }} > { children } < /AuthContext.Provider > ); } ; // コンテキストオブジェクトとProviderをexportする export { AuthContext , AuthProvider } ; createContext で作成した AuthContext と AuthProvider を外部に公開(export)することでコンテキストを利用しやすくしています。 ▶︎ クリックしてコード全文を見る // contexts/auth.tsx import React , { createContext , Dispatch , SetStateAction , useState , } from "react" ; interface Context { userId: number ; setUserId: Dispatch < SetStateAction < number >>; } const AuthContext = createContext < Context >( { userId: -1 , setUserId: () => {} , } ); const AuthProvider: React.FC = ( { children } ) => { const [ userId , setUserId ] = useState < number >( -1 ); return ( < AuthContext.Provider value = {{ userId , setUserId }} > { children } < /AuthContext.Provider > ); } ; // コンテキストオブジェクトとProviderをexportする export { AuthContext , AuthProvider } ; 作成した AuthProvider を App.tsx に記述します。 これによりアプリケーション内のどの コンポーネント でも useContext を使って AuthContext から値を購読できます。 // App.tsx import React from "react" ; import { AuthProvider } from "./contexts/auth" ; import LoginForm from "./components/LoginForm" ; import ToDoList from "./components/ToDoList" ; export default function App () : JSX. Element { return ( < AuthProvider > < div style = {{ padding: "8px" , textAlign: "center" }} > < LoginForm / > < ToDoList / > < /div > < /AuthProvider > ); } 実際に LoginForm と ToDoList コンポーネント でコンテキストを使ってみましょう。 まず LoginForm コンポーネント 内でフォーム送信時にコンテキストの userId を更新してみます。 // AuthContextからuserIdを更新する関数setUserIdを購読 const { setUserId } = useContext ( AuthContext ); // form要素のsubmitイベントを処理する関数 const submitHandler = ( e: FormEvent < HTMLFormElement >) => { e.preventDefault (); console .log ( "ログインボタン押下" , formData ); // AuthContextで配信される値userIdを更新 setUserId ( 1 ); } ; 次に ToDoList コンポーネント でコンテキストから userId を購読します。 そして useEffect で userId の状態を監視し、初期値(-1)でない場合に外部 API からリソースを取得するようにします。 // AuthContextからuserIdを購読 const { userId } = useContext ( AuthContext ); useEffect (() => { const f = async () => { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); setToDos ( json ); } ; // userId が初期値でない場合に外部APIコール if ( userId !== -1 ) { f (); } } , [ userId ] ); 以上でログインフォームのログインボタンを押下することで AuthContext の userId を更新し、その変更を検知して ToDo リストの情報を外部 API から取得する処理が実現できます。 useReducer useReducer は useState よりも複雑な状態を管理するためのフックです。 公式ドキュメントでは「 useState の代替品」として位置づけられています。 useReducer を使うための基本的なコードは以下のとおりです。 // `useState`の代替品。返り値はstateの変数とstateを更新するためのDispatch関数 const [ state , dispatch ] = useReducer ( reducer , initialArg , init ); useReducer を理解するためには 4 つの要素を理解する必要があります。 State … 状態 Reducer … State を更新するための関数 Action … State を更新するのに必要なデータ Dispatch … Action を Reducer に届ける関数 この 4 つの要素は図のような関係になります。 useReducer を使う ユースケース アプリケーション開発を進めていくと処理が複雑になるにつれて、管理しなければならない状態(state)が増えていきます。 また実際には、相互に関連する状態を更新しなければならない場面も増えます。 そのような場面で力を発揮するのが useReducer フックです。 例えば、先ほどの useEffect フックでを使った外部 API からのリソース取得を例に考えてみましょう。 外部リソースの取得では取得までに時間を要しますので読み込み中か否かを isLoading のような形で状態管理する必要があるでしょう。 さらにデータ取得時のエラーハンドリングを考えるとエラーが発生したかを error のような変数で状態管理する必要があります。 これらを useState フックで管理する場合は以下のようになります。 const [ todos , setToDos ] = useState < ToDo [] >( [] ); const [ isLoading , setIsLoading ] = useState < boolean >( true ); const [ error , setError ] = useState < boolean >( false ); このように複数の値に関連する状態を管理する場面で useReducer を使うことを考えます。 まず useReducer で管理する状態の型とその状態の初期値を定義します。 // 管理する状態の型 interface State { todoItems: ToDo [] ; isLoading: boolean ; error: boolean ; } // 状態の初期値 const initState: State = { todoItems: [] , isLoading: true , error: false , } ; 次に状態を更新するためのデータとなるアクションの型を定義します。 今回は状態を更新する操作として以下の 2 種類を考えます。 SET_TODOS … ToDo リストにアイテムをセットする操作。アクションは ToDo リストにセットするデータを含む。 SET_ERROR … エラーが発生した際にエ ラーフラ グを True にする操作。アクションはデータを持たない。 これらを型に起こします。 // アクションの種類 type ActionType = "SET_TODOS" | "SET_ERROR" ; // アクションの型 interface Action { type : ActionType ; payload?: ToDo [] ; } 上で定義した型を使って reducer 関数を作成します。 import { Reducer } from "react" ; const reducer: Reducer < State , Action > = ( state , action ) => { switch ( action. type) { case "SET_TODOS" : if ( ! action.payload ) { // payloadが含まれていなければエラー扱いにする return { ...state , error: true , isLoading: false , } ; } return { ...state , ...action.payload , isLoading: false , } ; case "SET_ERROR" : return { ...state , error: true , isLoading: false , } ; } } ; この reducer 関数と状態の初期値を使って useReducer を宣言します。 const [{ todoItems , error , isLoading } , dispatch ] = useReducer ( reducer , initState ); そして先程の useEffect 内で状態を更新していた部分を dispatch にアクションを渡すことで状態を更新するように書き換えます。 useEffect (() => { const f = async () => { try { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); dispatch ( { type : "SET_TODOS" , payload: { todoItems: json } } ); } catch ( e ) { console .log ( e ); dispatch ( { type : "SET_ERROR" } ); } } ; f (); } , [] ); この例だともともとがそこまで複雑な状態管理ではなかったため、 useReducer を使った記述が冗長だと感じるかもしれません。 どのタイミングで useReducer を使うのかは、個人/チーム次第ではありますが、うまく使うことで状態管理をわかりやすくできます。 ▶︎ クリックしてコード全文を見る // components/ToDoList.tsx import React , { Reducer , useEffect , useReducer } from "react" ; interface ToDo { id: number ; userId: number ; title: string ; completed: boolean ; } interface State { todoItems: ToDo [] ; isLoading: boolean ; error: boolean ; } const initState: State = { todoItems: [] , isLoading: true , error: false , } ; type ActionType = "SET_TODOS" | "SET_ERROR" ; interface Action { type : ActionType ; payload?: Partial < State >; } const reducer: Reducer < State , Action > = ( state , action ) => { switch ( action. type) { case "SET_TODOS" : if ( ! action.payload ) { // payloadが含まれていなければエラー扱いにする return { ...state , error: true , isLoading: false , } ; } return { ...state , ...action.payload.todoItems , isLoading: false , } ; case "SET_ERROR" : return { ...state , error: true , isLoading: false , } ; } } ; export default function ToDoList () : JSX. Element { const [{ todoItems , error , isLoading } , dispatch ] = useReducer ( reducer , initState ); useEffect (() => { const f = async () => { try { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); dispatch ( { type : "SET_TODOS" , payload: { todoItems: json } } ); } catch ( e ) { console .log ( e ); dispatch ( { type : "SET_ERROR" } ); } } ; f (); } , [] ); return ( <> { isLoading ? ( < p > ロード中です… < /p > ) : error ? ( < p > エラーが発生しました。 < /p > ) : ( < div style = {{ textAlign: "left" }} > { todoItems.map (( todoItem ) => ( < div key = { todoItem.id } style = {{ width: "250px" , border: "solid" , margin: "8px" , padding: "8px" , }} > < h4 > { todoItem.title } < /h4 > < p style = {{ textAlign: "right" }} > { todoItem.completed ? "✅ 完了" : "未実施" } < /p > < /div > )) } < /div > ) } < / > ); } useMemo useMemo は関数の返り値をメモ化するフックです。 メモ化はプログラムの最適化技法の 1 つで、計算結果を再利用するために保持して、再計算を防ぐものです。 そのため useMemo は最適化のためのフックという位置付けです。 useMemo を使うための基本的なコードは以下のとおりです。 // 返り値は関数の計算結果をメモ化した値 // 第2引数の依存配列に含まれる値が変更された時に再計算される const memoizedValue = useMemo < T >(() => computeExpensiveValue ( a , b ), [ a , b ] ); useMemo を使う ユースケース 基本的には最適化のためのフックですが、例えば配列を保持する state で配列を走査する処理が頻繁に必要な場合などに役立ちます。 ToDo リストの例で、一覧から完了済みのアイテムを useMemo によって取得することを考えてみましょう。 const [ todoItems , setToDos ] = useState < ToDo [] >( [] ); const completedItems = useMemo < ToDo [] >(() => { return todoItems.filter (( todoItem ) => todoItem.completed ); } , [ todos ] ); useMemo では依存配列に渡された state が更新された時にメモ化していた値を再計算します。 useCallback useCallback は関数をメモ化するフックです。 useCallback は最適化のためのフックという位置付けです。 そして useCallback を利用する場合は、基本的に React.memo と併用する必要があります。 React.memo と useCallback useCallback の話をする前に、 React.memo について簡単に説明します。 React の最上位 API - React.memo, https://ja.reactjs.org/docs/react-api.html#reactmemo すでに述べた通り、React では親 コンポーネント から子 コンポーネント に props を通してデータを渡します。 通常では、図中の点線で示した子 コンポーネント は親 コンポーネント が再描画されるタイミングで常に再描画されます。 React.memo はこの親 コンポーネント が再描画されるタイミングでの子 コンポーネント の再描画を最適化するものです。 React.memo では子 コンポーネント において、親 コンポーネント から受け取る props が再描画前の props と等価であれば、再描画をスキップします。つまり親 コンポーネント から子 コンポーネント に渡す props とその等価性が重要になります。 useCallback は props に渡す関数が等価であることを保証するためのフックです。 useCallback を使うための基本的なコードは以下のとおりです。 // 返り値はメモ化された関数 // 第2引数の依存配列に含まれる値が変更された時に再計算される const memoizedCallback = useCallback (() => { doSomething ( a , b ); } , [ a , b ] ); これにより子 コンポーネント では、props 受け取った関数が useCallback の第2引数の依存配列に含まれる値が変更されていない限りは等価なものとして扱えます。 useCallback を使う ユースケース ここまで説明した通り、 useCallback は最適化の流れで、子 コンポーネント の props に関数を渡す必要が生じた際に利用します。 React.memo は使用しませんが、props に関数を渡す場面を先ほどのログインフォームの例で見てみましょう。 まずフォームの状態を useState を使って定義していました。 const [ formData , setFormData ] = useState < SampleLoginForm >( { userId: "" , password: "" , } ); そして input 要素の onChange プロパティに関数を記述し formData の値を更新していました。 < input id = "userId" type= "text" name = "userId" placeholder = "ユーザーID" value = { formData.userId } onChange = { ( e ) => setFormData ( { ...formData , userId: e.target.value } ) } / > この onChange プロパティに渡す関数を useCallback で記述すると以下のようになります。 // inputタグのonChangeイベントを処理する関数 const onChangeHandler = useCallback (( e: ChangeEvent < HTMLInputElement >) => { setFormData (( prev: SampleLoginForm ) => { return { ...prev , [ e.target.name ] : e.target.value } ; } ); } , [] ); useCallback では第 2 引数の依存配列に含まれる値が変更されたタイミングで再度メモ化されるため、依存配列に含まれる値が少なくなるように意識する必要があります。 この例では、更新時に formData を参照せず、 setFormData 関数内で直前の formData の値を受けることよって依存配列が空になるようにしています。 これにより onChangeHandler 関数はメモ化が働き、 React.memo と併用した最適化ができます。 React Hooks を正しく使うために フックは一見すると JavaScript の関数ですが、正しく使う際には、ルールに従う必要があります。 特に useEffect や useMemo 、 useCallback といった依存配列を含むフックの使用では、依存関係の漏れによってバグを混入する恐れがあります。 フックを正しく利用するために、ESLint の eslint-plugin-react-hooks プラグイン を導入しておくことがお勧めです。 exhaustive-deps ルールを有効にすれば、依存配列が正しく記述されていない場合に警告を出すこともできます。 eslint-plugin-react-hooks https://www.npmjs.com/package/eslint-plugin-react-hooks おわりに 本記事では、React で提供される基本的な React Hooks をその ユースケース とともに紹介しました。 ここでは紹介できなかった React Hooks やカスタムフック、テスト方法なども今後、紹介できればと思います。 明日(12/14)は Toshihiro Nakamura さんから「Kotlinでデータベースアクセス」の記事が公開される予定です。 そちらもぜひご覧ください。 執筆: @yamada.y 、レビュー: @sato.taichi ( Shodo で執筆されました )
これは 電通国際情報サービス アドベントカレンダー の12日目の記事です。 こんにちは。 電通国際情報サービス 金融ソリューション事業部 石沢です。 本記事は当部門で5年ほど前から継続している組織としてのPostmortem(ポストモーテム)活動「失敗学」をご紹介します。様々な用語で類似の活動をされている組織も多いと思いますが、良き システム開発 を実施するためのヒントとなれば幸いです。 Postmortem(トラブル事後分析)文化について  Postmortemは直訳すると検死解剖ですが、 システム開発 の文脈では現在「トラブルの事後分析」という意味で使われています。有名なところでは サイトリライアビリティエンジニアリング The DevOps ハンドブック 理論・原則・実践のすべて 他多数の書籍で紹介されている概念です。 簡単にいうと、 システムやサービスにおいて発生したインシデント(障害等)の対応が完了した後に、 その内容や解消のために行われたアクション、 再発を防止するためのフォローアップについて、 文書化しレビュー、 共有すること を、 システム開発 の現場ではPostmortemと呼んでいます。障害報告書と似ていますが、顧客や上司に対する説明文書ではなく組織やチームが学習するために作成するものです。日本語だと ドリコムさんがポストモーテム例を公開 しているので、こちらを見るとより理解が深まるかもしれません。  特に近年、ITシステムそのものが複雑になったこと、およびITシステムを取り巻くビジネス環境も複雑になってきたことからPostmortemのような事後検証の重要性は高まっています。われわれは専門家として様々な技術・技法だけでなく、失敗からも学ばなければいけないというわけです。  Postmortemについてより詳しく学びたい場合は、 PagerDutyの公開しているガイド がオススメです。本文は英語ですが 機械翻訳 でもけっこう読めます。 SIer 組織におけるPostmortemの難しさ  「発生した問題から学ぶ」と言うと反対する人はほとんどいません。しかし、これを実行し定着させていくのはなかなか難しいものです。特に弊社のような システムインテグレーター 色の強い組織・部門では開発チームを取り巻くコンテキストが千差万別であるという課題があります。 受託開発、製品開発、サービス開発、研究開発など 顧客、 ドメイン も異なる 短期プロジェクト、長期プロジェクト、無期限のプロダクト開発など期間もバラバラ アジャイル から ウォーターフォール までバラエティに富んだ 開発プロセス 利用技術、言語なども様々  もちろんチーム別にPostmortemをやっていくのであれば問題はありません。ただそうすると、組織全体の学びの範囲がだいぶ狭まってしまいます。一方で当部門はお客様のミッション・クリティカルな システム開発 を担当することも多いため、特定のプロジェクトで発生した問題はできる限り広く横連携したいという想いもあります。事業会社の開発チームでPostmortemを行うのとは、ちょっと異なる悩みがありました。 勝手連 事故調(ジコチョー)モデル  様々なコンテキストが詰め込まれた システムインテグレーター 組織でPostmortemを行うことは難しいということを説明しました。この解決策として、当部門では「失敗学」で有名な畑村 洋太郎先生の提唱する「 勝手連 事故調」モデルを(勝手に)取り込ませていただいています。「 勝手連 事故調」は書籍「 失敗学実践講義 」で紹介されている、事故災害が発生した際に公的機関や特定企業が主体となって設立される 事故調査委員会 (事故調)とは異なる、 手弁当 で何の誓約も受けずに好き勝手に行う調査委員会のことです。「 勝手連 事故調」は「責任追及のための調査」ではない「原因究明のための科学的調査」を行います。この活動をモデルとして、当部門では題材となるシステム障害や開発時の失敗などについて、当事者とは異なる第 三者 を中心としたチームでPostmortemを行うようにしています。   勝手連 事故調モデルのメリットは次のようなものがあります。 責任追及のための調査となりにくい。第 三者 が中心に調査を行うので、当事者や利害関係者が深く関与すると働きがちな「チームの論理」(責任問題や、問題原因を人に帰属させがち)を排除できる 第 三者 が検討するので、当事者が非難されることはない 第 三者 の検討によって、当事者が気づきにくい組織課題や真因に近づきやすくなる(場合によっては、当事者が導出した結論と異なる分析結果を出すこともできる) 第 三者 が抽象化・一般化することを通じて、当事者チーム以外のメンバーでも理解できる教訓が抽出、共有できる 勝手連 事故調に情報が集約されることで、当事者に対して他のプロジェクトの対応事例などもアド バイス が可能  私の所属する部門では、この 勝手連 事故調モデルによるPostmortem活動(内部的には「失敗学分科会」)を運用しています。 現状の運用について  現在シニアからジュニアまで多様なメンバーで構成されたチームにて、以下のような運用を実施しています。 部門で発生した題材事象(システム障害事例や開発トラブルなど)を定期的に収集する 第 三者 の分析チームにて、題材を見ながらどのような論点があるか議論する(通称:味見) 第 三者 チームの中で担当を決めて、担当が ヒアリ ングと調査を行う 得られた情報を分析して、分析する。組織として学ぶべき教訓を抽出する 第 三者 チームで分析結果と教訓についてレビューする(通称:味わい) 得られた結果を取りまとめて、部門に広く共有する  そこそこ安定して運用できていますが、活動当初はいろいろありました。畑村 洋太郎先生が著書「失敗学のすすめ」等でも述べていますが、日本人は「失敗は恥」と考える文化があります。活動初期、発生トラブルの ヒアリ ングに行こうとすると担当者の上司から「あれは失敗ではなかったので、 ヒアリ ングはしないでほしい」とか「本人たちは十分に反省しているので、そっとしておいてほしい」などという意見を受けることもありました。これらはまさに「失敗は恥」という文化に起因するものでしょう。  活動継続により、この問題については組織として乗り越えることができました。いまはプロジェクトでトラブルが発生すると「おっ、これは失敗学行きだね」「しっかりと教訓にしていこう」という前向き(?)な会話も増えています。地道な活動によっていくらかは「失敗は恥」から「失敗は学びのチャンス」に変えられたと思います。  またプロジェクト横断の問題事例共有をしていく中で、トレンドの分析もできるようになりました。複数のプロジェクトで同じようなトラブルが発生していれば、それは組織としての教育施策やサポート体制の不足かもしれないと疑うことができます。必要に応じて臨時的に部署内で勉強会などを開催して補強するようになりました。  加えて、当初は想定しなかった意外な効果として、 勝手連 事故調である第 三者 チームが「学習の場」としても有効であることがわかりました。第 三者 チームの年次的にもスキル的にも多様なメンバーで、フレッシュなトラブルを題材に「どうやって分析するか」といった議論に始まり、「そういえば昔はこういう事がよくあったなぁ」「過去にこういった対応を実施したことがあった」などという組織の 暗黙知 の交換まで行われるようになりました。経験値の継承という意味でも良い場になっていると考えています。 今後の課題  現時点では大きな問題もなく運営出来ているのですが、中期的には第 三者 チームのメンバーが固定化しないようなローテーションは実施してく必要があります。あとは良い感じにデータベース化出来ていないのが悩みです。うまくやれば、イイ感じに解析や類推検索できるようになったりする気もするんですけどね。  本記事では失敗学を使ったPostmortem文化の定着の試みを紹介させていただきました。皆さんの組織ではどのような工夫をされていますか? ぜひ紹介いただければと思います。   電通国際情報サービス アドベントカレンダー はまだまだ続きます。明日のポストもぜひお楽しみください! 執筆: Ishizawa Kento (@kent) 、レビュー: @sato.taichi ( Shodo で執筆されました )
これは 電通国際情報サービス アドベントカレンダー の12日目の記事です。 こんにちは。 電通国際情報サービス 金融ソリューション事業部 石沢です。 本記事は当部門で5年ほど前から継続している組織としてのPostmortem(ポストモーテム)活動「失敗学」をご紹介します。様々な用語で類似の活動をされている組織も多いと思いますが、良き システム開発 を実施するためのヒントとなれば幸いです。 Postmortem(トラブル事後分析)文化について  Postmortemは直訳すると検死解剖ですが、 システム開発 の文脈では現在「トラブルの事後分析」という意味で使われています。有名なところでは サイトリライアビリティエンジニアリング The DevOps ハンドブック 理論・原則・実践のすべて 他多数の書籍で紹介されている概念です。 簡単にいうと、 システムやサービスにおいて発生したインシデント(障害等)の対応が完了した後に、 その内容や解消のために行われたアクション、 再発を防止するためのフォローアップについて、 文書化しレビュー、 共有すること を、 システム開発 の現場ではPostmortemと呼んでいます。障害報告書と似ていますが、顧客や上司に対する説明文書ではなく組織やチームが学習するために作成するものです。日本語だと ドリコムさんがポストモーテム例を公開 しているので、こちらを見るとより理解が深まるかもしれません。  特に近年、ITシステムそのものが複雑になったこと、およびITシステムを取り巻くビジネス環境も複雑になってきたことからPostmortemのような事後検証の重要性は高まっています。われわれは専門家として様々な技術・技法だけでなく、失敗からも学ばなければいけないというわけです。  Postmortemについてより詳しく学びたい場合は、 PagerDutyの公開しているガイド がオススメです。本文は英語ですが 機械翻訳 でもけっこう読めます。 SIer 組織におけるPostmortemの難しさ  「発生した問題から学ぶ」と言うと反対する人はほとんどいません。しかし、これを実行し定着させていくのはなかなか難しいものです。特に弊社のような システムインテグレーター 色の強い組織・部門では開発チームを取り巻くコンテキストが千差万別であるという課題があります。 受託開発、製品開発、サービス開発、研究開発など 顧客、 ドメイン も異なる 短期プロジェクト、長期プロジェクト、無期限のプロダクト開発など期間もバラバラ アジャイル から ウォーターフォール までバラエティに富んだ 開発プロセス 利用技術、言語なども様々  もちろんチーム別にPostmortemをやっていくのであれば問題はありません。ただそうすると、組織全体の学びの範囲がだいぶ狭まってしまいます。一方で当部門はお客様のミッション・クリティカルな システム開発 を担当することも多いため、特定のプロジェクトで発生した問題はできる限り広く横連携したいという想いもあります。事業会社の開発チームでPostmortemを行うのとは、ちょっと異なる悩みがありました。 勝手連 事故調(ジコチョー)モデル  様々なコンテキストが詰め込まれた システムインテグレーター 組織でPostmortemを行うことは難しいということを説明しました。この解決策として、当部門では「失敗学」で有名な畑村 洋太郎先生の提唱する「 勝手連 事故調」モデルを(勝手に)取り込ませていただいています。「 勝手連 事故調」は書籍「 失敗学実践講義 」で紹介されている、事故災害が発生した際に公的機関や特定企業が主体となって設立される 事故調査委員会 (事故調)とは異なる、 手弁当 で何の誓約も受けずに好き勝手に行う調査委員会のことです。「 勝手連 事故調」は「責任追及のための調査」ではない「原因究明のための科学的調査」を行います。この活動をモデルとして、当部門では題材となるシステム障害や開発時の失敗などについて、当事者とは異なる第 三者 を中心としたチームでPostmortemを行うようにしています。   勝手連 事故調モデルのメリットは次のようなものがあります。 責任追及のための調査となりにくい。第 三者 が中心に調査を行うので、当事者や利害関係者が深く関与すると働きがちな「チームの論理」(責任問題や、問題原因を人に帰属させがち)を排除できる 第 三者 が検討するので、当事者が非難されることはない 第 三者 の検討によって、当事者が気づきにくい組織課題や真因に近づきやすくなる(場合によっては、当事者が導出した結論と異なる分析結果を出すこともできる) 第 三者 が抽象化・一般化することを通じて、当事者チーム以外のメンバーでも理解できる教訓が抽出、共有できる 勝手連 事故調に情報が集約されることで、当事者に対して他のプロジェクトの対応事例などもアド バイス が可能  私の所属する部門では、この 勝手連 事故調モデルによるPostmortem活動(内部的には「失敗学分科会」)を運用しています。 現状の運用について  現在シニアからジュニアまで多様なメンバーで構成されたチームにて、以下のような運用を実施しています。 部門で発生した題材事象(システム障害事例や開発トラブルなど)を定期的に収集する 第 三者 の分析チームにて、題材を見ながらどのような論点があるか議論する(通称:味見) 第 三者 チームの中で担当を決めて、担当が ヒアリ ングと調査を行う 得られた情報を分析して、分析する。組織として学ぶべき教訓を抽出する 第 三者 チームで分析結果と教訓についてレビューする(通称:味わい) 得られた結果を取りまとめて、部門に広く共有する  そこそこ安定して運用できていますが、活動当初はいろいろありました。畑村 洋太郎先生が著書「失敗学のすすめ」等でも述べていますが、日本人は「失敗は恥」と考える文化があります。活動初期、発生トラブルの ヒアリ ングに行こうとすると担当者の上司から「あれは失敗ではなかったので、 ヒアリ ングはしないでほしい」とか「本人たちは十分に反省しているので、そっとしておいてほしい」などという意見を受けることもありました。これらはまさに「失敗は恥」という文化に起因するものでしょう。  活動継続により、この問題については組織として乗り越えることができました。いまはプロジェクトでトラブルが発生すると「おっ、これは失敗学行きだね」「しっかりと教訓にしていこう」という前向き(?)な会話も増えています。地道な活動によっていくらかは「失敗は恥」から「失敗は学びのチャンス」に変えられたと思います。  またプロジェクト横断の問題事例共有をしていく中で、トレンドの分析もできるようになりました。複数のプロジェクトで同じようなトラブルが発生していれば、それは組織としての教育施策やサポート体制の不足かもしれないと疑うことができます。必要に応じて臨時的に部署内で勉強会などを開催して補強するようになりました。  加えて、当初は想定しなかった意外な効果として、 勝手連 事故調である第 三者 チームが「学習の場」としても有効であることがわかりました。第 三者 チームの年次的にもスキル的にも多様なメンバーで、フレッシュなトラブルを題材に「どうやって分析するか」といった議論に始まり、「そういえば昔はこういう事がよくあったなぁ」「過去にこういった対応を実施したことがあった」などという組織の 暗黙知 の交換まで行われるようになりました。経験値の継承という意味でも良い場になっていると考えています。 今後の課題  現時点では大きな問題もなく運営出来ているのですが、中期的には第 三者 チームのメンバーが固定化しないようなローテーションは実施してく必要があります。あとは良い感じにデータベース化出来ていないのが悩みです。うまくやれば、イイ感じに解析や類推検索できるようになったりする気もするんですけどね。  本記事では失敗学を使ったPostmortem文化の定着の試みを紹介させていただきました。皆さんの組織ではどのような工夫をされていますか? ぜひ紹介いただければと思います。   電通国際情報サービス アドベントカレンダー はまだまだ続きます。明日のポストもぜひお楽しみください! 執筆: Ishizawa Kento (@kent) 、レビュー: @sato.taichi ( Shodo で執筆されました )
ISID X イノベーション 本部の山下です。 このポストは 電通国際情報サービス Advent Calendar 2021 の11日目のポストです。 中途入社したばかりで右も左も分からないのですが、楽しそうな企画ですので参加させていただくことにしました! 今回は自分が普段よく使っている プログラミング言語 「 Common Lisp 」の紹介記事を書かせていただきます。 はじめに 皆さん、 Common Lisp という プログラミング言語 をご存じでしょうか? 「括弧が多い」と有名な Lisp の方言の一つです。 情報系の学部などでは習ったことがあるかたもいらっしゃるかもしれません。 何はともあれ、 Common Lisp で書かれたHello, Worldを見てみましょう。 ( format t "Hello, World~%" ) ちょっと独特な感じの書き方になっていますね。 Common Lisp や Lisp 系の言語では、S式と呼ばれるリストを使ってプログラムを記述します。 関数呼び出しはもちろん、すべての構文がこのS式と呼ばれるリストで記述されます。 なお、 Common Lisp では、 ; 以降はコメントになります。 ;; これはコメントです。`;` 以降の文字はすべてコメントになります ;; 関数呼び出し ( 関数名 引数1 引数2 引数3 ) ;; if式 ;; 最初の式 (equal var1 0)の評価結果が真であるなら ;; (format t "var1 is zero")が実行されて ;; そうでないなら ;; (format t "var1 is not zero")が実行されます ( if ( equal var1 0 ) ( format t "var1 is zero" ) ( format t "var1 is not zero" )) 関数呼び出しやifなどの特殊な構文(special formと呼びます)でも基本的に同じ形のS式になっています。 ;; Common Lispでは基本的に以下のような式だけでプログラムが記述できます ( 関数名か構文を示す文字 引数1 引数2 .... ) 最初はとっつきづらいかもしれません。しかしS式の書き方は1種類しかないので些末な文法で迷うことなくなります。 このため慣れるとロジックに注力してプログラムを書けるようになってきます。 また Lisp で本当に実用的なプログラムが動いているのを見たことがないという声もたまに伺います。 そこで、今回はこの Common Lisp を使って実用的なプログラムの例として、 REST API を書く例をご紹介しようかと思います。 実用的なプログラムって書けるの? では、早速ですが、 REST API サーバを Common Lisp で記述してみましょう。 ゼロから全部実装するのは大変なので、他の言語と同じようにライブラリを使って実装してみます。 ( ql:quickload '( :ningle :clack )) quickloadという、 Python のpip、 Java の Maven のようなライブラリマネージャを利用してWebサーバ関連のライブラリを読み込みます。 ;; サーバの実体をグローバル変数として宣言、作成する ( defvar *app* ( make-instance ' ningle:<app> )) ;; ;; "/"にGETアクセスすると、Hello, World!が返ってくるAPI ;; ;; - setfは代入を実行する構文です ;; - (ningle:route *app* "/" :method :GET) はwebフレームワークのningleの提供する ;; アクセスがあった際に呼ばれるコールバック関数です ;; - lambda式は無名関数を宣言する構文です ;; 以下の式は、setf式を使って、"/"にGETアクセスがあった際に呼ばれるコールバック関数を、 ;; lambda式を用いて宣言した無名関数で上書き代入するということになります。 ( setf ( ningle:route *app* "/" :method :GET ) # '( lambda ( params ) "Hello, World!" )) ;; ;; "/hello"にPOSTアクセスした際のAPI実装 ;; このAPIは、usernameというパラメータを与えて実行すると ;; "hello, 与えたusername" と返し ;; パラメータがない場合には、 ;; hello, somebody と返すような内容となっています ;; ;; 以下の式は、上で作成した"/"に対するAPIと同様に、"/hello"に対するPOSTアクセスが行われた際の ;; コールバック関数の上書きを行っています。 ;; ここでは、POST時の引数の処理を行うために、無名関数の宣言(lambda式)の中で ;; let式という変数を作成する構文を利用しています。 ;; ( setf ( ningle:route *app* "/hello" :method :POST ) # '( lambda ( params ) ;; let式を用いて、usernameという変数を宣言し、 ;; POST時のパラメータ(username)の値で初期化しています。 ( let (( username ( cdr ( assoc "username" params :test #'string= )))) ( if ( not ( null username )) ( format nil "Hello, ~A" username ) "Hello, somebody." )))) ( clack:clackup *app* ) これで完成です! 簡単ですね。 では早速アクセスしてみます。 まずはGETで"/"(ルート)にアクセスしてみます。 $ curl -s http://localhost:5000/ Hello, World! ちゃんとHello, Worldと返ってきましたね。 今度はPOSTで"/hello"にアクセスしてみましょう。 $ curl -s -XPOST -d "username=Yamashita" localhost:5000/hello Hello, Yamashita $ curl -s -XPOST localhost:5000/hello Hello, somebody. ちゃんと与えたパラメータを元に応答を返してくれました! REST API として正しく動作してそうですね。 IDE などについて Emacs で開発するのが一般的なのですが、VisualStudioCodeの プラグイン なども開発が進んでいるようです。 手元で試した限りだと小規模な開発では十分実用になりそうです。 https://marketplace.visualstudio.com/items?itemName=ailisp.commonlisp-vscode Emacs を利用する場合はSLIMEという定番の IDE 環境があります。 Emacs の キーバインド が苦手でないかたはこちらをお勧めします。 https://common-lisp.net/project/slime/ まとめ 今回は Common Lisp の簡単な紹介と、ライブラリを利用して REST API サーバの簡単な実装例を紹介してみました。 どうでしょうか、とっつきづらそうな Common Lisp ですが、簡単な読み方を覚えてしまえばそんなに括弧が気にならなくなったのではないでしょうか? また、 REST API なども実装できる十分に実用的な プログラミング言語 であるということも紹介できたのではないかと思います。 この記事で、 Common Lisp のことを括弧が多いだけの言語じゃなく、身近な問題を解決できる普通の プログラミング言語 だと思っていただければ幸いです。 お勧めの参考書など 最後に、 Common Lisp を学習するためのお勧めの書籍を紹介して終わろうかと思います。 Land of Lisp Common Lisp の入門から始まった一通りの使い方が学べる内容となっています。 挿絵の Lisp Alien がかわいいですね。 実践 Common Lisp タイトルの通り実践的な話に関して一通り書かれている書籍になります。 例えば、ファイルの入出力といった実際のプログラムを書く上では必要となるような情報がまとまっています。 このあたりの書籍が Common Lisp の入門にはお勧めです。 執筆: @yamashita.tsuyoshi ( Shodo で執筆されました )
ISID X イノベーション 本部の山下です。 このポストは 電通国際情報サービス Advent Calendar 2021 の11日目のポストです。 中途入社したばかりで右も左も分からないのですが、楽しそうな企画ですので参加させていただくことにしました! 今回は自分が普段よく使っている プログラミング言語 「 Common Lisp 」の紹介記事を書かせていただきます。 はじめに 皆さん、 Common Lisp という プログラミング言語 をご存じでしょうか? 「括弧が多い」と有名な Lisp の方言の一つです。 情報系の学部などでは習ったことがあるかたもいらっしゃるかもしれません。 何はともあれ、 Common Lisp で書かれたHello, Worldを見てみましょう。 ( format t "Hello, World~%" ) ちょっと独特な感じの書き方になっていますね。 Common Lisp や Lisp 系の言語では、S式と呼ばれるリストを使ってプログラムを記述します。 関数呼び出しはもちろん、すべての構文がこのS式と呼ばれるリストで記述されます。 なお、 Common Lisp では、 ; 以降はコメントになります。 ;; これはコメントです。`;` 以降の文字はすべてコメントになります ;; 関数呼び出し ( 関数名 引数1 引数2 引数3 ) ;; if式 ;; 最初の式 (equal var1 0)の評価結果が真であるなら ;; (format t "var1 is zero")が実行されて ;; そうでないなら ;; (format t "var1 is not zero")が実行されます ( if ( equal var1 0 ) ( format t "var1 is zero" ) ( format t "var1 is not zero" )) 関数呼び出しやifなどの特殊な構文(special formと呼びます)でも基本的に同じ形のS式になっています。 ;; Common Lispでは基本的に以下のような式だけでプログラムが記述できます ( 関数名か構文を示す文字 引数1 引数2 .... ) 最初はとっつきづらいかもしれません。しかしS式の書き方は1種類しかないので些末な文法で迷うことなくなります。 このため慣れるとロジックに注力してプログラムを書けるようになってきます。 また Lisp で本当に実用的なプログラムが動いているのを見たことがないという声もたまに伺います。 そこで、今回はこの Common Lisp を使って実用的なプログラムの例として、 REST API を書く例をご紹介しようかと思います。 実用的なプログラムって書けるの? では、早速ですが、 REST API サーバを Common Lisp で記述してみましょう。 ゼロから全部実装するのは大変なので、他の言語と同じようにライブラリを使って実装してみます。 ( ql:quickload '( :ningle :clack )) quickloadという、 Python のpip、 Java の Maven のようなライブラリマネージャを利用してWebサーバ関連のライブラリを読み込みます。 ;; サーバの実体をグローバル変数として宣言、作成する ( defvar *app* ( make-instance ' ningle:<app> )) ;; ;; "/"にGETアクセスすると、Hello, World!が返ってくるAPI ;; ;; - setfは代入を実行する構文です ;; - (ningle:route *app* "/" :method :GET) はwebフレームワークのningleの提供する ;; アクセスがあった際に呼ばれるコールバック関数です ;; - lambda式は無名関数を宣言する構文です ;; 以下の式は、setf式を使って、"/"にGETアクセスがあった際に呼ばれるコールバック関数を、 ;; lambda式を用いて宣言した無名関数で上書き代入するということになります。 ( setf ( ningle:route *app* "/" :method :GET ) # '( lambda ( params ) "Hello, World!" )) ;; ;; "/hello"にPOSTアクセスした際のAPI実装 ;; このAPIは、usernameというパラメータを与えて実行すると ;; "hello, 与えたusername" と返し ;; パラメータがない場合には、 ;; hello, somebody と返すような内容となっています ;; ;; 以下の式は、上で作成した"/"に対するAPIと同様に、"/hello"に対するPOSTアクセスが行われた際の ;; コールバック関数の上書きを行っています。 ;; ここでは、POST時の引数の処理を行うために、無名関数の宣言(lambda式)の中で ;; let式という変数を作成する構文を利用しています。 ;; ( setf ( ningle:route *app* "/hello" :method :POST ) # '( lambda ( params ) ;; let式を用いて、usernameという変数を宣言し、 ;; POST時のパラメータ(username)の値で初期化しています。 ( let (( username ( cdr ( assoc "username" params :test #'string= )))) ( if ( not ( null username )) ( format nil "Hello, ~A" username ) "Hello, somebody." )))) ( clack:clackup *app* ) これで完成です! 簡単ですね。 では早速アクセスしてみます。 まずはGETで"/"(ルート)にアクセスしてみます。 $ curl -s http://localhost:5000/ Hello, World! ちゃんとHello, Worldと返ってきましたね。 今度はPOSTで"/hello"にアクセスしてみましょう。 $ curl -s -XPOST -d "username=Yamashita" localhost:5000/hello Hello, Yamashita $ curl -s -XPOST localhost:5000/hello Hello, somebody. ちゃんと与えたパラメータを元に応答を返してくれました! REST API として正しく動作してそうですね。 IDE などについて Emacs で開発するのが一般的なのですが、VisualStudioCodeの プラグイン なども開発が進んでいるようです。 手元で試した限りだと小規模な開発では十分実用になりそうです。 https://marketplace.visualstudio.com/items?itemName=ailisp.commonlisp-vscode Emacs を利用する場合はSLIMEという定番の IDE 環境があります。 Emacs の キーバインド が苦手でないかたはこちらをお勧めします。 https://common-lisp.net/project/slime/ まとめ 今回は Common Lisp の簡単な紹介と、ライブラリを利用して REST API サーバの簡単な実装例を紹介してみました。 どうでしょうか、とっつきづらそうな Common Lisp ですが、簡単な読み方を覚えてしまえばそんなに括弧が気にならなくなったのではないでしょうか? また、 REST API なども実装できる十分に実用的な プログラミング言語 であるということも紹介できたのではないかと思います。 この記事で、 Common Lisp のことを括弧が多いだけの言語じゃなく、身近な問題を解決できる普通の プログラミング言語 だと思っていただければ幸いです。 お勧めの参考書など 最後に、 Common Lisp を学習するためのお勧めの書籍を紹介して終わろうかと思います。 Land of Lisp Common Lisp の入門から始まった一通りの使い方が学べる内容となっています。 挿絵の Lisp Alien がかわいいですね。 実践 Common Lisp タイトルの通り実践的な話に関して一通り書かれている書籍になります。 例えば、ファイルの入出力といった実際のプログラムを書く上では必要となるような情報がまとまっています。 このあたりの書籍が Common Lisp の入門にはお勧めです。 執筆: @yamashita.tsuyoshi ( Shodo で執筆されました )
これは 電通国際情報サービス Advent Calendar 2021 の10日目の記事です。 はじめに 利用技術 開発環境 エディターの作成 UIコンポーネントを配置する エディターで記述したC#スクリプトを実行する 実行結果のオブジェクトをフォーマットする スクリプトの実行例外を表示する コンパイルエラーを表示する .NET APIのインテリセンスを表示する 自作したAPIのインテリセンスを表示し、実行する インスタンスメンバーを実行する まとめ はじめに はじめまして! 電通国際情報サービス (ISID) 製造ソリューション事業部の余部です。 構想設計支援システム iQUAVIS (アイクアビス)の開発を担当しています。 iQUAVISは自社でス クラッチ 開発している WPF アプリケーションです。機能を独自に拡張できる プラグイン を作成するための SDK として、多くの API があります。これを簡単にテストできるよう、アプリケーション上で C# を記述して API を実行できるエディターをテスト用の プラグイン として開発しました。これによってテスト効率が大幅に上がり、 SDK の開発で欠かせない機能になっています。 今回は、 WPF で C# を実行するエディターを作成する方法を紹介します。 利用技術 Roslyn C# と Visual Basic の コンパイラ を オープンソース で実装した、.NETの コンパイラ プラットフォームです。コードを静的解析するための API も備えています。 RoslynPad SharpDevelop で使われている AvalonEdit をベースにした C# のエディターです。文字通りRoslynを使って実装されています。 独自のアプリにエディターを組み込めるよう、NuGetパッケージとしても公開されています。 開発環境 Visual Studio 2019 .NET 5.0 C# 9.0 エディターの作成 UI コンポーネント を配置する C# のコードを記述するTextBox、コードを実行するButton、実行結果を表示する読み取り専用TextBoxを XAML で記述します。 <TextBox x : Name = "CodeEditor" AcceptsReturn = "True" /> <Button Click = "Button_Click" Content = "実行" /> <TextBox x : Name = "ResultTextBox" IsReadOnly = "True" TextWrapping = "Wrap" /> ※ Gridなどのレイアウト要素は省略しています。 エディターで記述した C# スクリプト を実行する まず、 Microsoft.CodeAnalysis.CSharp.Scripting のパッケージをインストールします。 次に、実行ボタンのクリックで Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.EvaluateAsync を使用して C# スクリプト を実行し、結果をResultTextBoxに表示します。 private async void Button_Click( object sender, RoutedEventArgs e) { var result = await CSharpScript.EvaluateAsync(CodeEditor.Text); ResultTextBox.Text = result?.ToString(); } これで、CodeEditorに記述した C# を実行できるようになりました。 試しに、次のようなコードを実行しましょう。評価したい行には、末尾に ; を付けないでください。 System.Environment.Version 次のような結果が表示されます。 5.0.12 実行結果のオブジェクトをフォーマットする 先ほどの例では、実行結果の表示を ToString で文字列に変換しました。この場合は ToString を実装していないオブジェクトでは適切な結果が表示されません。 そこで、 Microsoft.CodeAnalysis.CSharp.Scripting.Hosting.CSharpObjectFormatter を使ってフォーマットします。 ResultTextBox.Text = CSharpObjectFormatter.Instance.FormatObject(result); 次のようなコードを実行しましょう。 record struct Person( string Name, int Age); new Person( "Shohei" , 27 ) オブジェクトのデータがわかるようにフォーマットされました。 [Person { Name = Shohei, Age = 27 }] ちなみに、 record struct は C# 10の構文です。 Microsoft.CodeAnalysis.CSharp.Scripting のバージョン4.0では C# 10に対応しているので、 C# 9のアプリケーションでも C# 10の構文が使えます。次のページにRoslynで使用可能な C# のバージョンが記載されています。 https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning スクリプト の実行例外を表示する 次のようなコードを実行しましょう。 string .Concat( null ) ArgumentNullExceptionがスローされてしまいます。実行例外をハンドリングしてResultTextBoxに表示しましょう。 コードを実行する CSharpScript.EvaluateAsync が例外をスローするため、例外をキャッチすることもできますが、例外クラスを特定できないため、 System.Exception をキャッチすることになります。そのような汎用的な例外のキャッチは避けたいところです。ここでは Microsoft.CodeAnalysis.Scripting.Script<T>.RunAsync を使うことで、例外をスローせずに戻り値の ScriptState から例外を取得できます。 var state = await CSharpScript.Create(CodeEditor.Text).RunAsync(catchException: _ => true ); var formatter = CSharpObjectFormatter.Instance; ResultTextBox.Text = state.Exception == null ? formatter.FormatObject(state.ReturnValue) : formatter.FormatException(state.Exception); 例外オブジェクトのフォーマットにも、先ほどの CSharpObjectFormatter が使えます。 これで、実行結果に例外を表示できるようになりました。先ほどのコードを再実行してみましょう。ResultTextBoxに例外の詳細が表示されます。 System.ArgumentNullException: Value cannot be null. (Parameter 'values') + string.Concat(string[]) + <Initialize>.MoveNext() 例外の原因がわかりやすくなりましたね。 コンパイル エラーを表示する 次は、実行するコードが コンパイル エラーの場合を考慮しましょう。例えば、括弧が足りないコードを実行してみましょう。 string .Concat( "a" , "b" Microsoft.CodeAnalysis.Scripting.CompilationErrorException の例外がスローされてしまいます。先に CSharpScript.Create の結果を コンパイル することで、例外をスローせずに コンパイル エラーを取得できます。 var script = CSharpScript.Create(CodeEditor.Text); var diagnostics = script.Compile(); if (diagnostics.Any(x => x.Severity == DiagnosticSeverity.Error)) { ResultTextBox.Text = string .Join(Environment.NewLine, diagnostics); return ; } var state = await script.RunAsync(catchException: _ => true ); ...(省略) 先ほどのコードを再実行してみましょう。ResultTextBoxに コンパイル エラーの原因が表示されます。 (1,23): error CS1026: ) が必要です。 以上でコードを実行できるエディターができました。しかし、CodeEditorは単なるTextBoxのため、インテリセンスがありません。補完がないエディターで C# を書くのは辛いものです。 そこで、インテリセンスを実装しましょう。 .NET API のインテリセンスを表示する まず、 RoslynPad.Editor.Windows のパッケージをインストールします。 現時点のRoslynPadの最新版 RoslynPad.Editor.Windows 1.2.0 ではRoslynの最新版に対応していないため、先にインストールした Microsoft.CodeAnalysis.CSharp.Scripting を3.6に下げる必要があります。 RoslynPad.Editor.Windows の依存関係で Microsoft.CodeAnalysis.CSharp.Scripting もインストールされるため、 Microsoft.CodeAnalysis.CSharp.Scripting をアンインストールしてもOKです。 次に、 XAML のTextBoxを RoslynCodeEditor に置き換えます。 <!-- RoslynPadのプレフィックスを定義する --> xmlns:roslyn="clr-namespace:RoslynPad.Editor;assembly=RoslynPad.Editor.Windows" ...(省略) < roslyn : RoslynCodeEditor x : Name = "CodeEditor" Loaded = "CodeEditor_Loaded" /> RoslynCodeEditor はコードビハインドで初期化する必要があります。次のように、ロード時に初期化できます。 private void CodeEditor_Loaded( object sender, RoutedEventArgs e) { var roslynPadAssemblies = new [] { Assembly.Load( "RoslynPad.Roslyn.Windows" ), Assembly.Load( "RoslynPad.Editor.Windows" ) }; var assemblies = new [] { Assembly.Load( "System.Private.CoreLib" ) }; var roslynHost = new RoslynHost( roslynPadAssemblies, RoslynHostReferences.NamespaceDefault.With(assemblyReferences: assemblies)); CodeEditor.Initialize(roslynHost, new ClassificationHighlightColors(), Directory.GetCurrentDirectory(), string .Empty); } 最初のRoslynPad アセンブリ の設定は必須です。設定しないと RoslynCodeEditor.Initialize で CompositionFailedException が発生します。次の System.Private.CoreLib の アセンブリ は、インテリセンスで表示したいものを指定します。 これで、CoreLib API のインテリセンスが表示されるようになりました。 先ほどの Assembly.Load では、 アセンブリ 名を文字列で指定しましたが、 アセンブリ 内に存在する任意のクラスを指定してタイプセーフでも記述できます。 var roslynPadAssemblies = new [] { typeof (RoslynCodeEditor).Assembly, // RoslynPad.Editor.Windows typeof (GlyphExtensions).Assembly, // RoslynPad.Roslyn.Windows }; var assemblies = new [] { typeof ( object ).Assembly, // System.Private.CoreLib }; 自作した API のインテリセンスを表示し、実行する 次は、自作した API を実行できるようにします。例えば、次のようなクラスを作成します。 namespace Custom { public static class Api { public static string Hello( string name) => $ "Hello {name}!" ; } } これを実行できるようにするには、 CSharpScript.Create で アセンブリ を指定する必要があります。インテリセンスも表示したいので、 RoslynHost にも指定します。両者の アセンブリ 設定を共 通化 するため、 コンストラクター で RoslynHost を生成してメンバーに保持するなどして、設定を共有します。 private readonly RoslynHost _roslynHost; public MainWindow() { ...(省略) var assemblies = new [] { typeof ( object ).Assembly Assembly.GetExecutingAssembly(), // 自作したAPIのアセンブリを設定する }; _roslynHost = new RoslynHost( roslynPadAssemblies, RoslynHostReferences.NamespaceDefault.With(assemblyReferences: assemblies)); } private void CodeEditor_Loaded( object sender, RoutedEventArgs e) { CodeEditor.Initialize(_roslynHost, new ClassificationHighlightColors(), Directory.GetCurrentDirectory(), string .Empty); } 次に、 CSharpScript.Create で アセンブリ の設定を追加します。 var scriptOptions = ScriptOptions.Default.WithReferences(_roslynHost.DefaultReferences); var script = CSharpScript.Create(CodeEditor.Text, scriptOptions); これで、作成した Custom.Api.Hello が実行できるようになりました。 インスタンス メンバーを実行する これまでは、静的メンバーを実行してきました。CodeEditorから内部で保持している インスタンス にアクセスできると、さらに便利になります。ここでは、内部で利用している RoslynHost のメンバーを実行できるようにします。 CSharpScript では、 スクリプト が グローバル変数 としてアクセスできる インスタンス を設定できます。次のように、 CSharpScript.Create で インスタンス の型を、 Script.RunAsync で インスタンス を設定します。 var script = CSharpScript.Create(CodeEditor.Text, scriptOptions, _roslynHost.GetType()); ...(省略) var state = await script.RunAsync(_roslynHost, _ => true ); インテリセンスに表示するのは、少々やっかいです。次のように RoslynHost を継承したクラスを作成し、 CreateProject のオーバーライドで インスタンス の型を含んだプロジェクトを追加する必要があります。 public class CustomRoslynHost : RoslynHost { private readonly Type _targetType; public CustomRoslynHost( Type targetType, IEnumerable<Assembly> additionalAssemblies = null , RoslynHostReferences references = null , ImmutableArray< string >? disabledDiagnostics = null ) : base (additionalAssemblies, references, disabledDiagnostics) { _targetType = targetType; } protected override Project CreateProject(Solution solution, DocumentCreationArgs args, CompilationOptions compilationOptions, Project previousProject = null ) { var projectId = ProjectId.CreateNewId(); var projectInfo = ProjectInfo.Create( projectId, VersionStamp.Create(), "MyProject" , "MyAssembly" , LanguageNames.CSharp, compilationOptions: compilationOptions, parseOptions: new CSharpParseOptions(kind: SourceCodeKind.Script), metadataReferences: DefaultReferences, isSubmission: true , hostObjectType: _targetType); return solution.AddProject(projectInfo).GetProject(projectId); } } _roslynHost に設定していた インスタンス をCustomRoslynHostに置き換えましょう。 これで、インテリセンスを表示できるようになりました。例えば、 _roslynHost のメンバーである DefaultImports を実行できます。 まとめ この記事では、 WPF で C# エディターを作成する方法を紹介しました。RoslynとRoslynPadを使えば、短いコードで実装できますね!これで、皆さんのアプリケーションにも C# エディターを組み込むことができます。我々のように API のテストに利用するなど、有用なケースがありましたら、ぜひ試してみてください。 最後まで読んでいただき、ありがとうございました! 執筆: @amabe.haruaki 、レビュー: @sato.taichi ( Shodo で執筆されました )
これは 電通国際情報サービス Advent Calendar 2021 の10日目の記事です。 はじめに 利用技術 開発環境 エディターの作成 UIコンポーネントを配置する エディターで記述したC#スクリプトを実行する 実行結果のオブジェクトをフォーマットする スクリプトの実行例外を表示する コンパイルエラーを表示する .NET APIのインテリセンスを表示する 自作したAPIのインテリセンスを表示し、実行する インスタンスメンバーを実行する まとめ はじめに はじめまして! 電通国際情報サービス (ISID) 製造ソリューション事業部の余部です。 構想設計支援システム iQUAVIS (アイクアビス)の開発を担当しています。 iQUAVISは自社でス クラッチ 開発している WPF アプリケーションです。機能を独自に拡張できる プラグイン を作成するための SDK として、多くの API があります。これを簡単にテストできるよう、アプリケーション上で C# を記述して API を実行できるエディターをテスト用の プラグイン として開発しました。これによってテスト効率が大幅に上がり、 SDK の開発で欠かせない機能になっています。 今回は、 WPF で C# を実行するエディターを作成する方法を紹介します。 利用技術 Roslyn C# と Visual Basic の コンパイラ を オープンソース で実装した、.NETの コンパイラ プラットフォームです。コードを静的解析するための API も備えています。 RoslynPad SharpDevelop で使われている AvalonEdit をベースにした C# のエディターです。文字通りRoslynを使って実装されています。 独自のアプリにエディターを組み込めるよう、NuGetパッケージとしても公開されています。 開発環境 Visual Studio 2019 .NET 5.0 C# 9.0 エディターの作成 UI コンポーネント を配置する C# のコードを記述するTextBox、コードを実行するButton、実行結果を表示する読み取り専用TextBoxを XAML で記述します。 <TextBox x : Name = "CodeEditor" AcceptsReturn = "True" /> <Button Click = "Button_Click" Content = "実行" /> <TextBox x : Name = "ResultTextBox" IsReadOnly = "True" TextWrapping = "Wrap" /> ※ Gridなどのレイアウト要素は省略しています。 エディターで記述した C# スクリプト を実行する まず、 Microsoft.CodeAnalysis.CSharp.Scripting のパッケージをインストールします。 次に、実行ボタンのクリックで Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.EvaluateAsync を使用して C# スクリプト を実行し、結果をResultTextBoxに表示します。 private async void Button_Click( object sender, RoutedEventArgs e) { var result = await CSharpScript.EvaluateAsync(CodeEditor.Text); ResultTextBox.Text = result?.ToString(); } これで、CodeEditorに記述した C# を実行できるようになりました。 試しに、次のようなコードを実行しましょう。評価したい行には、末尾に ; を付けないでください。 System.Environment.Version 次のような結果が表示されます。 5.0.12 実行結果のオブジェクトをフォーマットする 先ほどの例では、実行結果の表示を ToString で文字列に変換しました。この場合は ToString を実装していないオブジェクトでは適切な結果が表示されません。 そこで、 Microsoft.CodeAnalysis.CSharp.Scripting.Hosting.CSharpObjectFormatter を使ってフォーマットします。 ResultTextBox.Text = CSharpObjectFormatter.Instance.FormatObject(result); 次のようなコードを実行しましょう。 record struct Person( string Name, int Age); new Person( "Shohei" , 27 ) オブジェクトのデータがわかるようにフォーマットされました。 [Person { Name = Shohei, Age = 27 }] ちなみに、 record struct は C# 10の構文です。 Microsoft.CodeAnalysis.CSharp.Scripting のバージョン4.0では C# 10に対応しているので、 C# 9のアプリケーションでも C# 10の構文が使えます。次のページにRoslynで使用可能な C# のバージョンが記載されています。 https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning スクリプト の実行例外を表示する 次のようなコードを実行しましょう。 string .Concat( null ) ArgumentNullExceptionがスローされてしまいます。実行例外をハンドリングしてResultTextBoxに表示しましょう。 コードを実行する CSharpScript.EvaluateAsync が例外をスローするため、例外をキャッチすることもできますが、例外クラスを特定できないため、 System.Exception をキャッチすることになります。そのような汎用的な例外のキャッチは避けたいところです。ここでは Microsoft.CodeAnalysis.Scripting.Script<T>.RunAsync を使うことで、例外をスローせずに戻り値の ScriptState から例外を取得できます。 var state = await CSharpScript.Create(CodeEditor.Text).RunAsync(catchException: _ => true ); var formatter = CSharpObjectFormatter.Instance; ResultTextBox.Text = state.Exception == null ? formatter.FormatObject(state.ReturnValue) : formatter.FormatException(state.Exception); 例外オブジェクトのフォーマットにも、先ほどの CSharpObjectFormatter が使えます。 これで、実行結果に例外を表示できるようになりました。先ほどのコードを再実行してみましょう。ResultTextBoxに例外の詳細が表示されます。 System.ArgumentNullException: Value cannot be null. (Parameter 'values') + string.Concat(string[]) + <Initialize>.MoveNext() 例外の原因がわかりやすくなりましたね。 コンパイル エラーを表示する 次は、実行するコードが コンパイル エラーの場合を考慮しましょう。例えば、括弧が足りないコードを実行してみましょう。 string .Concat( "a" , "b" Microsoft.CodeAnalysis.Scripting.CompilationErrorException の例外がスローされてしまいます。先に CSharpScript.Create の結果を コンパイル することで、例外をスローせずに コンパイル エラーを取得できます。 var script = CSharpScript.Create(CodeEditor.Text); var diagnostics = script.Compile(); if (diagnostics.Any(x => x.Severity == DiagnosticSeverity.Error)) { ResultTextBox.Text = string .Join(Environment.NewLine, diagnostics); return ; } var state = await script.RunAsync(catchException: _ => true ); ...(省略) 先ほどのコードを再実行してみましょう。ResultTextBoxに コンパイル エラーの原因が表示されます。 (1,23): error CS1026: ) が必要です。 以上でコードを実行できるエディターができました。しかし、CodeEditorは単なるTextBoxのため、インテリセンスがありません。補完がないエディターで C# を書くのは辛いものです。 そこで、インテリセンスを実装しましょう。 .NET API のインテリセンスを表示する まず、 RoslynPad.Editor.Windows のパッケージをインストールします。 現時点のRoslynPadの最新版 RoslynPad.Editor.Windows 1.2.0 ではRoslynの最新版に対応していないため、先にインストールした Microsoft.CodeAnalysis.CSharp.Scripting を3.6に下げる必要があります。 RoslynPad.Editor.Windows の依存関係で Microsoft.CodeAnalysis.CSharp.Scripting もインストールされるため、 Microsoft.CodeAnalysis.CSharp.Scripting をアンインストールしてもOKです。 次に、 XAML のTextBoxを RoslynCodeEditor に置き換えます。 <!-- RoslynPadのプレフィックスを定義する --> xmlns:roslyn="clr-namespace:RoslynPad.Editor;assembly=RoslynPad.Editor.Windows" ...(省略) < roslyn : RoslynCodeEditor x : Name = "CodeEditor" Loaded = "CodeEditor_Loaded" /> RoslynCodeEditor はコードビハインドで初期化する必要があります。次のように、ロード時に初期化できます。 private void CodeEditor_Loaded( object sender, RoutedEventArgs e) { var roslynPadAssemblies = new [] { Assembly.Load( "RoslynPad.Roslyn.Windows" ), Assembly.Load( "RoslynPad.Editor.Windows" ) }; var assemblies = new [] { Assembly.Load( "System.Private.CoreLib" ) }; var roslynHost = new RoslynHost( roslynPadAssemblies, RoslynHostReferences.NamespaceDefault.With(assemblyReferences: assemblies)); CodeEditor.Initialize(roslynHost, new ClassificationHighlightColors(), Directory.GetCurrentDirectory(), string .Empty); } 最初のRoslynPad アセンブリ の設定は必須です。設定しないと RoslynCodeEditor.Initialize で CompositionFailedException が発生します。次の System.Private.CoreLib の アセンブリ は、インテリセンスで表示したいものを指定します。 これで、CoreLib API のインテリセンスが表示されるようになりました。 先ほどの Assembly.Load では、 アセンブリ 名を文字列で指定しましたが、 アセンブリ 内に存在する任意のクラスを指定してタイプセーフでも記述できます。 var roslynPadAssemblies = new [] { typeof (RoslynCodeEditor).Assembly, // RoslynPad.Editor.Windows typeof (GlyphExtensions).Assembly, // RoslynPad.Roslyn.Windows }; var assemblies = new [] { typeof ( object ).Assembly, // System.Private.CoreLib }; 自作した API のインテリセンスを表示し、実行する 次は、自作した API を実行できるようにします。例えば、次のようなクラスを作成します。 namespace Custom { public static class Api { public static string Hello( string name) => $ "Hello {name}!" ; } } これを実行できるようにするには、 CSharpScript.Create で アセンブリ を指定する必要があります。インテリセンスも表示したいので、 RoslynHost にも指定します。両者の アセンブリ 設定を共 通化 するため、 コンストラクター で RoslynHost を生成してメンバーに保持するなどして、設定を共有します。 private readonly RoslynHost _roslynHost; public MainWindow() { ...(省略) var assemblies = new [] { typeof ( object ).Assembly Assembly.GetExecutingAssembly(), // 自作したAPIのアセンブリを設定する }; _roslynHost = new RoslynHost( roslynPadAssemblies, RoslynHostReferences.NamespaceDefault.With(assemblyReferences: assemblies)); } private void CodeEditor_Loaded( object sender, RoutedEventArgs e) { CodeEditor.Initialize(_roslynHost, new ClassificationHighlightColors(), Directory.GetCurrentDirectory(), string .Empty); } 次に、 CSharpScript.Create で アセンブリ の設定を追加します。 var scriptOptions = ScriptOptions.Default.WithReferences(_roslynHost.DefaultReferences); var script = CSharpScript.Create(CodeEditor.Text, scriptOptions); これで、作成した Custom.Api.Hello が実行できるようになりました。 インスタンス メンバーを実行する これまでは、静的メンバーを実行してきました。CodeEditorから内部で保持している インスタンス にアクセスできると、さらに便利になります。ここでは、内部で利用している RoslynHost のメンバーを実行できるようにします。 CSharpScript では、 スクリプト が グローバル変数 としてアクセスできる インスタンス を設定できます。次のように、 CSharpScript.Create で インスタンス の型を、 Script.RunAsync で インスタンス を設定します。 var script = CSharpScript.Create(CodeEditor.Text, scriptOptions, _roslynHost.GetType()); ...(省略) var state = await script.RunAsync(_roslynHost, _ => true ); インテリセンスに表示するのは、少々やっかいです。次のように RoslynHost を継承したクラスを作成し、 CreateProject のオーバーライドで インスタンス の型を含んだプロジェクトを追加する必要があります。 public class CustomRoslynHost : RoslynHost { private readonly Type _targetType; public CustomRoslynHost( Type targetType, IEnumerable<Assembly> additionalAssemblies = null , RoslynHostReferences references = null , ImmutableArray< string >? disabledDiagnostics = null ) : base (additionalAssemblies, references, disabledDiagnostics) { _targetType = targetType; } protected override Project CreateProject(Solution solution, DocumentCreationArgs args, CompilationOptions compilationOptions, Project previousProject = null ) { var projectId = ProjectId.CreateNewId(); var projectInfo = ProjectInfo.Create( projectId, VersionStamp.Create(), "MyProject" , "MyAssembly" , LanguageNames.CSharp, compilationOptions: compilationOptions, parseOptions: new CSharpParseOptions(kind: SourceCodeKind.Script), metadataReferences: DefaultReferences, isSubmission: true , hostObjectType: _targetType); return solution.AddProject(projectInfo).GetProject(projectId); } } _roslynHost に設定していた インスタンス をCustomRoslynHostに置き換えましょう。 これで、インテリセンスを表示できるようになりました。例えば、 _roslynHost のメンバーである DefaultImports を実行できます。 まとめ この記事では、 WPF で C# エディターを作成する方法を紹介しました。RoslynとRoslynPadを使えば、短いコードで実装できますね!これで、皆さんのアプリケーションにも C# エディターを組み込むことができます。我々のように API のテストに利用するなど、有用なケースがありましたら、ぜひ試してみてください。 最後まで読んでいただき、ありがとうございました! 執筆: @amabe.haruaki 、レビュー: @sato.taichi ( Shodo で執筆されました )
ISID X(クロス) イノベーション 本部 アドバンスドテク ノロ ジー 部の米谷です。本記事は 電通国際情報サービス Advent Calendar 2021 の9日目のポストです。 私は現在、 Microsoft Azure を使ったデータ分析基盤の案件支援や研究開発の業務を行っています。本記事では、個人的に最近注目している DataOps というキーワードについて書いていきたいと思います。 DataOps とは? DataOps の必要性 DataOps で利用する Azure サービス Azure データ分析基盤における DataOps DataOps のために進化し続ける Azure サービス まとめ DataOps とは? DataOps とは「データの利用者と管理者が協力してツールやプロセス、組織文化を継続的に改善していく仕組み」という DevOps や MLOps のデータ分析基盤版ともいうべきものです。核となる要素技術としてはデータカタログや オーケストレーション ツールの他、 Infrastracture as Code (IaC) や CI/CD といった DevOps や MLOps と共通するものもあります。 DataOps は2018年ごろから注目され始め、2021年現在はガートナーのハイプ・サイクルでも黎明期から流行期に移りつつあり、少しずつではありますが認知度が広がり始めています。 Azure のサービス群でも DataOps の実践に役立つ新サービスやアップデートが増えており、この点も自分が注目している理由となります。 DataOps の必要性 そもそも論として DataOps はなぜ必要なのか?というところですが、最近お客様とデータ分析基盤についてお話すると スモールスタートで徐々に拡張していきたい というご要望を多くいただきます。これには以下のような背景があります。 全ての利用者/部門の要件を取りまとめてから構築となるとスピード感が出ないため、限られた利用者/部門を対象として始めそのフィードバックも活かしながら少しずつ拡張していきたい。 クラウド の特徴を活かし、サイジングなどは利用状況を見ながら適宜スケールさせていきたい。 これらの要望に対して アジャイル というキーワードが出ることもあるのですが、目的に対する実現手段という意味で個人的にはむしろ DataOps の方がマッチするのではと考えています。 ※このあたりの違いについて書き始めると長くなるため、本記事では割愛します。ご了承ください。 DataOps で利用する Azure サービス DataOps が求められる背景を理解できたところで、 Azure のデータ分析基盤で DataOps をどのように行っていけばよいかを考えてみたいと思います。まず、 Azure のデータ分析基盤を構成するサービス群を列挙すると以下のようになります。 用途 サービス データレイク Azure Data Lake Storage Gen2 データウェアハウス Azure Synapse Analytics ETL / オーケストレーション Azure Data Factory データカタログ/データガバナンス Azure Purview 上記はデータ分析基盤として使われるであろう必要最小限の構成となります。これ以外でも要件によって Azure IoT Hub や Azure Databricks 、 Power BI などを使われるかもしれませんが以降の説明には大きく影響しないため割愛します。 また、 DataOps の実践には ソースコード 管理や CI/CD も必須となります。これらについては、 Azure DevOps や GitHub / GitHub Actions などを環境に応じて選定する形となり、このツールを使わなければいけないというものはないです。私は GitHub / GitHub Actions を使うことが多いですが、メンバーのスキルセットや希望なども考慮して都度決めています。 Azure データ分析基盤における DataOps 利用サービスが整理できましたので、実践方法について説明します。先ほどスモールスタートの例としてあげた「利用者/部門の増加」の場合、新規利用者/部門向けに分析用データを新しく収集し加工した後に BI ダッシュ ボード用のテーブル/ビューを追加することになります。よって Azure の環境は以下のような影響を受けます。 変更内容 影響を受けるサービス データソースの追加 Azure Data Factory ETL ジョブの追加 Azure Data Factory テーブル/ビューの追加 Azure Synapse Analytics つまり、これらのサービス定義をあらかじめコード化しておき、要件に応じた追加・修正を行った後に CI/CD を回し環境をアップデートしていくことで DataOps が実現されることになります。文章で書くとシンプルですが、実際はそうとも限りません。 例えば ソースコード 一つ取っても、上記の中には ARM テンプレートで管理されるものもあれば SQL スクリプト のものもあります。CI/CD のパイプラインもレビューやテストをどのように組み込むのかを考えなければいけませんし、それらに対して一定の正解はなく都度検討となります。 一つ言えることは、初めから完璧な仕組みを整える必要はなく、できるところから少しずつ始めていけばよいのではということです。小さく始めて組織の中で徐々に育てていくことが、 DataOps の本質にも通ずるのではないかと思います。 DataOps のために進化し続ける Azure サービス 冒頭で「 Azure で DataOps の実践に役立つ新サービスやアップデートが増えている」と書きましたが、ここで一つ具体例として Azure Data Factory を紹介します。 Azure Data Factory は ETL ジョブを GUI で開発できるサービスです。コーディングレスな実装が可能な一方でこれまでは修正変更なども全て画面上で行わなければならず、またバージョン履歴も持っていなかったため継続的な開発やメンテナンスという面では課題を抱えていました。 この Azure Data Factory がアップデートされ、 Git リポジトリ との連携が可能になりました。 Azure Data Factory のソース管理 Git 連携には ソースコード 管理の他 GitHub Actions などによる パイプライン連携も含まれます。つまりこのアップデートにより GUI での開発生産性を維持しつつ CI/CD を回すという DataOps 実践のための仕組みが整備されたといえます。 このように Azure のサービスは常にアップデートされているため、使い続けることで少しずつ自分たちの環境が理想的な DataOps の姿に近づいていくことが期待されます。 まとめ 本記事は Azure データ分析基盤での DataOps の実践方法について解説しました。今回は概要についての記述のみでしたが、 DataOps は非常に奥の深いテーマです。 Microsoft Docs や GitHub にも参考となるリファレンスがあるのですが、非常に重厚で私自身まだ完全には読み切れていません。 最新のデータ ウェアハウスの DataOps DataOps - Parking Sensor Demo これらについても理解を進め、そのエッセンスを自身の関わる案件に少しずつ適用していけるといいなと思います。 最後までお読みいただきありがとうございました。 ISID の アドベントカレンダー はまだまだ続きますので、明日以降もお楽しみに! 執筆: @yoneya.fumihiko 、レビュー: @nakamura.toshihiro ( Shodo で執筆されました )
ISID X(クロス) イノベーション 本部 アドバンスドテク ノロ ジー 部の米谷です。本記事は 電通国際情報サービス Advent Calendar 2021 の9日目のポストです。 私は現在、 Microsoft Azure を使ったデータ分析基盤の案件支援や研究開発の業務を行っています。本記事では、個人的に最近注目している DataOps というキーワードについて書いていきたいと思います。 DataOps とは? DataOps の必要性 DataOps で利用する Azure サービス Azure データ分析基盤における DataOps DataOps のために進化し続ける Azure サービス まとめ DataOps とは? DataOps とは「データの利用者と管理者が協力してツールやプロセス、組織文化を継続的に改善していく仕組み」という DevOps や MLOps のデータ分析基盤版ともいうべきものです。核となる要素技術としてはデータカタログや オーケストレーション ツールの他、 Infrastracture as Code (IaC) や CI/CD といった DevOps や MLOps と共通するものもあります。 DataOps は2018年ごろから注目され始め、2021年現在はガートナーのハイプ・サイクルでも黎明期から流行期に移りつつあり、少しずつではありますが認知度が広がり始めています。 Azure のサービス群でも DataOps の実践に役立つ新サービスやアップデートが増えており、この点も自分が注目している理由となります。 DataOps の必要性 そもそも論として DataOps はなぜ必要なのか?というところですが、最近お客様とデータ分析基盤についてお話すると スモールスタートで徐々に拡張していきたい というご要望を多くいただきます。これには以下のような背景があります。 全ての利用者/部門の要件を取りまとめてから構築となるとスピード感が出ないため、限られた利用者/部門を対象として始めそのフィードバックも活かしながら少しずつ拡張していきたい。 クラウド の特徴を活かし、サイジングなどは利用状況を見ながら適宜スケールさせていきたい。 これらの要望に対して アジャイル というキーワードが出ることもあるのですが、目的に対する実現手段という意味で個人的にはむしろ DataOps の方がマッチするのではと考えています。 ※このあたりの違いについて書き始めると長くなるため、本記事では割愛します。ご了承ください。 DataOps で利用する Azure サービス DataOps が求められる背景を理解できたところで、 Azure のデータ分析基盤で DataOps をどのように行っていけばよいかを考えてみたいと思います。まず、 Azure のデータ分析基盤を構成するサービス群を列挙すると以下のようになります。 用途 サービス データレイク Azure Data Lake Storage Gen2 データウェアハウス Azure Synapse Analytics ETL / オーケストレーション Azure Data Factory データカタログ/データガバナンス Azure Purview 上記はデータ分析基盤として使われるであろう必要最小限の構成となります。これ以外でも要件によって Azure IoT Hub や Azure Databricks 、 Power BI などを使われるかもしれませんが以降の説明には大きく影響しないため割愛します。 また、 DataOps の実践には ソースコード 管理や CI/CD も必須となります。これらについては、 Azure DevOps や GitHub / GitHub Actions などを環境に応じて選定する形となり、このツールを使わなければいけないというものはないです。私は GitHub / GitHub Actions を使うことが多いですが、メンバーのスキルセットや希望なども考慮して都度決めています。 Azure データ分析基盤における DataOps 利用サービスが整理できましたので、実践方法について説明します。先ほどスモールスタートの例としてあげた「利用者/部門の増加」の場合、新規利用者/部門向けに分析用データを新しく収集し加工した後に BI ダッシュ ボード用のテーブル/ビューを追加することになります。よって Azure の環境は以下のような影響を受けます。 変更内容 影響を受けるサービス データソースの追加 Azure Data Factory ETL ジョブの追加 Azure Data Factory テーブル/ビューの追加 Azure Synapse Analytics つまり、これらのサービス定義をあらかじめコード化しておき、要件に応じた追加・修正を行った後に CI/CD を回し環境をアップデートしていくことで DataOps が実現されることになります。文章で書くとシンプルですが、実際はそうとも限りません。 例えば ソースコード 一つ取っても、上記の中には ARM テンプレートで管理されるものもあれば SQL スクリプト のものもあります。CI/CD のパイプラインもレビューやテストをどのように組み込むのかを考えなければいけませんし、それらに対して一定の正解はなく都度検討となります。 一つ言えることは、初めから完璧な仕組みを整える必要はなく、できるところから少しずつ始めていけばよいのではということです。小さく始めて組織の中で徐々に育てていくことが、 DataOps の本質にも通ずるのではないかと思います。 DataOps のために進化し続ける Azure サービス 冒頭で「 Azure で DataOps の実践に役立つ新サービスやアップデートが増えている」と書きましたが、ここで一つ具体例として Azure Data Factory を紹介します。 Azure Data Factory は ETL ジョブを GUI で開発できるサービスです。コーディングレスな実装が可能な一方でこれまでは修正変更なども全て画面上で行わなければならず、またバージョン履歴も持っていなかったため継続的な開発やメンテナンスという面では課題を抱えていました。 この Azure Data Factory がアップデートされ、 Git リポジトリ との連携が可能になりました。 Azure Data Factory のソース管理 Git 連携には ソースコード 管理の他 GitHub Actions などによる パイプライン連携も含まれます。つまりこのアップデートにより GUI での開発生産性を維持しつつ CI/CD を回すという DataOps 実践のための仕組みが整備されたといえます。 このように Azure のサービスは常にアップデートされているため、使い続けることで少しずつ自分たちの環境が理想的な DataOps の姿に近づいていくことが期待されます。 まとめ 本記事は Azure データ分析基盤での DataOps の実践方法について解説しました。今回は概要についての記述のみでしたが、 DataOps は非常に奥の深いテーマです。 Microsoft Docs や GitHub にも参考となるリファレンスがあるのですが、非常に重厚で私自身まだ完全には読み切れていません。 最新のデータ ウェアハウスの DataOps DataOps - Parking Sensor Demo これらについても理解を進め、そのエッセンスを自身の関わる案件に少しずつ適用していけるといいなと思います。 最後までお読みいただきありがとうございました。 ISID の アドベントカレンダー はまだまだ続きますので、明日以降もお楽しみに! 執筆: @yoneya.fumihiko 、レビュー: @nakamura.toshihiro ( Shodo で執筆されました )
皆さんこんにちは。製造ソリューション事業部の長坂です。 この記事は、 電通国際情報サービス Advent Calendar 2021 の8日目の記事になります。昨日の記事は、福竹さんの「 ふりかえり入門した、ふりかえり 」でした。こちらもぜひご覧ください。 本日は、私がISIDに入社して初めて担当した案件から学んだことについて、自身の振り返りの意味も込めてご紹介したいと思います。 はじめに 長坂自身の紹介 社会人歴:2年目 担当ソリューション:自社製品の導入・維持保守・ プラグイン 開発など。 基本的には、ソフトウェア開発ではなく、どちらかというと導入側メインで携わっている人間です。なので、業務上コードを書いたりすることはほとんどないのが現状です。 担当した案件の紹介 今回、とある プラグイン 製品のPoCを実施しました。 PoC(Proof of Concept:概念実証)とは、新しいア イデア やコンセプトを実際に小規模で運用することで、実現可能性や得られる効果などについて検証する手法です。 通常のPoCでは、以下のプロセスで実施していくと考えています。 顧客の課題を ヒアリ ング 課題に対応する提案を実施 プロトタイプ開発 ユーザーによる効果検証 3,4を繰り返す しかし、今回以下のようなプロセスをとりました。 とある研究結果からコンセプト立案し、製品を開発 上記のコンセプトを情シス部門に提案 協力していただく現場ユーザーを探す 現場ユーザーによる効果検証 検証結果を製品開発にフィードバック プロセスは違えど、PoC、または製品を導入するにあたって共通することを自分の学びとして記載しています。 結果、お客様での導入には至りませんでしたが、製品に対するフィードバックを得られました。そのため、PoCを実施した意義はあったと考えています。 というわけで、早速学んだことについてご紹介します。 その1 ユーザーの ユースケース をしっかり把握し、使い方を提案しよう! PoCに手を挙げてくださったユーザー全員が、積極的に参加するとは限りません。 今回のPoCでは、一時期は1人しか使っていただけない時期もあり、どうしたら使ってもらえるのか相当悩みました。 なぜ使っていないのかユーザーへ ヒアリ ングした結果、「使う場面がユーザー側も良く分かっていない」ことが、原因ということが分かりました。 ユースケース を考え、実行するのはユーザーの役割かもしれませんが、 使い方のヒントをユーザーに渡すのは導入エンジニア側の役割 です。 ただ機能の説明をするのではなく、 ユーザーが現在置かれた状況(仕事のやり方)を把握 し、 今ある課題を解決する方法 課題ベースにとどまらず、新たな ユースケース を提案 していくことが必要です。 ユーザーの仕事のやり方を把握するためには、 - どれくらいの頻度で関係する情報を得る・更新するのか - 誰と情報を共有して仕事を進めるのか - 情報を得る・更新するのに二重で実施していることはないか このあたりを意識すると、良い ユースケース を考案するカギとなると思っています。 また、ちゃんと筋の通った提案をしても、ユーザーに刺さらないことがよくあります。 100発打って1発当たればいいや、という気持ちで 、思いつく限り使い方を提案しましょう。 その2 「導入すること」が成功と思わない! ビジネスにおける最初のゴールは、お客様に製品を導入してもらうことです。 ※その先にシステムの維持管理や継続して使ってもらうための努力は必要ですが、いったん置いておきます。 しかし、今回のPoCでは「自分の中のゴール=ビジネスのゴール」としてしまい、大変な思いをしました。 PoCのゴールは、「機能や ユースケース が顧客の使い方にマッチしているか確かめること」です。 導入には至らなくても、製品改善のフィードバックを得るだけでそれは導入してもらうためのステップを着実に踏んでいることになります。 「 自分の中のゴール=何か身になる学びを得る 」としたほうが良いでしょう。 その3 ユーザーへの会話は積極的に仕掛けよう! その1にもつながる話ですが、 ユースケース を知っているのも改善のフィードバックを得るのも、情報を持っているのはすべて使ってもらうユーザーです。 ユーザーに話を聞くことで、自分が今まで分からなかったことが分かります。またはそのヒントを得られます。 「何か一つでも情報が得られれば」と思いながら質問や情報共有、提案などをしていくことが大切です。 理由や聞きたいこともなく会話を仕掛けるのは、相手の時間を無駄にするのでよくありません。事前にポイントを整理しておきましょう。 上記はビジネスの基本ですが、PoCにおいてももちろん当てはまります。 特にPoCでは、「学びが多ければ多いほど良い」と考えているので、 「積極的にユーザーに話を聞きに行く」 ことが大切だと学びました。 まとめ 今回は、初めて担当した案件でPoCを通じて学んだことを3つお伝えしました。ベテランエンジニアの方にはすべて当たり前と感じられるかもしれませんが、私にとってはすべてが新鮮な学びでした。 初めてながら主担当で実施した案件だったため、色々と大変な思いをしましたが、振り返ると自分が成長する良い機会になったと思います。 テクニカルな話題が多い中で、少し異色のテックブログとなりましたが、いかがでしたか? 明日の記事は、米谷さんの「データ分析基盤/DataOpsに関する何か」です。お楽しみに! 執筆: @nagasaka.takuro 、レビュー: @sato.taichi ( Shodo で執筆されました )
皆さんこんにちは。製造ソリューション事業部の長坂です。 この記事は、 電通国際情報サービス Advent Calendar 2021 の8日目の記事になります。昨日の記事は、福竹さんの「 ふりかえり入門した、ふりかえり 」でした。こちらもぜひご覧ください。 本日は、私がISIDに入社して初めて担当した案件から学んだことについて、自身の振り返りの意味も込めてご紹介したいと思います。 はじめに 長坂自身の紹介 社会人歴:2年目 担当ソリューション:自社製品の導入・維持保守・ プラグイン 開発など。 基本的には、ソフトウェア開発ではなく、どちらかというと導入側メインで携わっている人間です。なので、業務上コードを書いたりすることはほとんどないのが現状です。 担当した案件の紹介 今回、とある プラグイン 製品のPoCを実施しました。 PoC(Proof of Concept:概念実証)とは、新しいア イデア やコンセプトを実際に小規模で運用することで、実現可能性や得られる効果などについて検証する手法です。 通常のPoCでは、以下のプロセスで実施していくと考えています。 顧客の課題を ヒアリ ング 課題に対応する提案を実施 プロトタイプ開発 ユーザーによる効果検証 3,4を繰り返す しかし、今回以下のようなプロセスをとりました。 とある研究結果からコンセプト立案し、製品を開発 上記のコンセプトを情シス部門に提案 協力していただく現場ユーザーを探す 現場ユーザーによる効果検証 検証結果を製品開発にフィードバック プロセスは違えど、PoC、または製品を導入するにあたって共通することを自分の学びとして記載しています。 結果、お客様での導入には至りませんでしたが、製品に対するフィードバックを得られました。そのため、PoCを実施した意義はあったと考えています。 というわけで、早速学んだことについてご紹介します。 その1 ユーザーの ユースケース をしっかり把握し、使い方を提案しよう! PoCに手を挙げてくださったユーザー全員が、積極的に参加するとは限りません。 今回のPoCでは、一時期は1人しか使っていただけない時期もあり、どうしたら使ってもらえるのか相当悩みました。 なぜ使っていないのかユーザーへ ヒアリ ングした結果、「使う場面がユーザー側も良く分かっていない」ことが、原因ということが分かりました。 ユースケース を考え、実行するのはユーザーの役割かもしれませんが、 使い方のヒントをユーザーに渡すのは導入エンジニア側の役割 です。 ただ機能の説明をするのではなく、 ユーザーが現在置かれた状況(仕事のやり方)を把握 し、 今ある課題を解決する方法 課題ベースにとどまらず、新たな ユースケース を提案 していくことが必要です。 ユーザーの仕事のやり方を把握するためには、 - どれくらいの頻度で関係する情報を得る・更新するのか - 誰と情報を共有して仕事を進めるのか - 情報を得る・更新するのに二重で実施していることはないか このあたりを意識すると、良い ユースケース を考案するカギとなると思っています。 また、ちゃんと筋の通った提案をしても、ユーザーに刺さらないことがよくあります。 100発打って1発当たればいいや、という気持ちで 、思いつく限り使い方を提案しましょう。 その2 「導入すること」が成功と思わない! ビジネスにおける最初のゴールは、お客様に製品を導入してもらうことです。 ※その先にシステムの維持管理や継続して使ってもらうための努力は必要ですが、いったん置いておきます。 しかし、今回のPoCでは「自分の中のゴール=ビジネスのゴール」としてしまい、大変な思いをしました。 PoCのゴールは、「機能や ユースケース が顧客の使い方にマッチしているか確かめること」です。 導入には至らなくても、製品改善のフィードバックを得るだけでそれは導入してもらうためのステップを着実に踏んでいることになります。 「 自分の中のゴール=何か身になる学びを得る 」としたほうが良いでしょう。 その3 ユーザーへの会話は積極的に仕掛けよう! その1にもつながる話ですが、 ユースケース を知っているのも改善のフィードバックを得るのも、情報を持っているのはすべて使ってもらうユーザーです。 ユーザーに話を聞くことで、自分が今まで分からなかったことが分かります。またはそのヒントを得られます。 「何か一つでも情報が得られれば」と思いながら質問や情報共有、提案などをしていくことが大切です。 理由や聞きたいこともなく会話を仕掛けるのは、相手の時間を無駄にするのでよくありません。事前にポイントを整理しておきましょう。 上記はビジネスの基本ですが、PoCにおいてももちろん当てはまります。 特にPoCでは、「学びが多ければ多いほど良い」と考えているので、 「積極的にユーザーに話を聞きに行く」 ことが大切だと学びました。 まとめ 今回は、初めて担当した案件でPoCを通じて学んだことを3つお伝えしました。ベテランエンジニアの方にはすべて当たり前と感じられるかもしれませんが、私にとってはすべてが新鮮な学びでした。 初めてながら主担当で実施した案件だったため、色々と大変な思いをしましたが、振り返ると自分が成長する良い機会になったと思います。 テクニカルな話題が多い中で、少し異色のテックブログとなりましたが、いかがでしたか? 明日の記事は、米谷さんの「データ分析基盤/DataOpsに関する何か」です。お楽しみに! 執筆: @nagasaka.takuro 、レビュー: @sato.taichi ( Shodo で執筆されました )
 こんにちは。ISID XI 本部 AI トランスフォーメーションセンター所属の福竹です。  本記事は ISIDアドベントカレンダー記事 の7日目の記事となります。 これは何?(TL;DR) Why ふりかえり?(ふりかえりに興味を持つまで) What is ふりかえり?(ふりかえりって何?) Y:やったこと(やっていること) 1. きちんと時間をとって、開発スプリント毎のふりかえり(スプリントレトロスペクティブ)をやる(やった) 2. 開発業務以外にも、現状認識のチェックやアイデア出しにKPTやEffort&Pain等のフレームワークを使ってみた W:わかったこと(気付き) 基本はKPT。しかし書き方や時間配分は基本を守ったほうが良い。 ツールはなんだっていい。ただ、参加者にとって平等で、直感的に使いやすいことは大事。 アレンジする時は「何が目的か」「迷いなくスコープを見つめてもらえるか」を考えておくことが大事。 リモートファシリテーションに「ふりかえり」は効く。 T:つぎやること これは何?(TL;DR)  ふりかえり初心者が「ふりかえり」に興味を持ち、今年やって試したこと考えたことのふりかえりです。色々書きますが、コアメッセージは一つ「ふりかえりはいいぞ」に尽きます。 Why ふりかえり?(ふりかえりに興味を持つまで)   私の所属する組織 では、内製& スクラム でプロダクト開発をしています。10人弱の開発チーム全員が何らかプロダクトの開発者であり スクラム メンバーというチーム構成です。筆者個人は今年この開発チームにジョインし、現在はプロダクトの1つの スクラム マスター(修行中)を務めています。  ……とは言ったものの、私自身 スクラム 開発は現職が初めての経験で、個人的に本年はキャッチアップと試行錯誤の年でした。少しでもベース知識を 仕入 れようと スクラム 関連の書籍を読み漁っていたのですが、そんな折に誘われた社内の読書会(※仕掛け人はアドカレ6日目の橋詰さん)で本記事を書くきっかけの一冊に行き当たります。 アジャイルなチームをつくる ふりかえりガイドブック 始め方・ふりかえりの型・手法・マインドセット  「ふりかえり」という手法が色々と琴線に触れた事もあり、本書片手にチームメンバーの協力をいただきながら試行錯誤した事が、本日の本記事のネタになります。 What is ふりかえり?(ふりかえりって何?)  本記事はHow toを主眼にしないので詳しくは割愛させてください。具体感としては、「YWT」「 KPT 」「FUN/DONE/LEARN」等の、 アジャイル レトロスペクティブに端を発する(そして実際のところ アジャイル 開発関係なく活用できる)アクティビティを想定しています。詳しくは是非とも参考書籍&情報を御覧ください。 Y:やったこと(やっていること)  やったことそのものはシンプルです。 1. きちんと時間をとって、開発スプリント毎のふりかえり(スプリントレトロスペクティブ)をやる(やった)  最初に気付いた事は、担当製品のチームで既存メンバーが忙しすぎて、数ヶ月ほどふりかえり(スプリントレトロスペクティブ)が止まっていた事でした。そこで開発の山を超えたあたりで私が無邪気に「ふりかえりやりたい!」と言い出して、月1程度の頻度で、ふりかえりを設定して実施することにしました。(そして言い出しっぺの権限で好きに アジェンダ を組ませて貰いました)。基本的な事ですが、けっこう状況としてはあるあるだと思います。  とはいえ最初は右も左も分からないので、「ふりかえりガイドブック」付属の「 チートシート 」を参考に恐る恐る アジェンダ を組みました。以後も基本的には一部を組み替えつつ、 チートシート を参考にしています。拙くて恐縮ですが、 アジェンダ 例(45分)はこんな感じです。私がそうだったのですが、やりはじめは具体例を知りたくなると思うので載せておきます。(2回目以降も、基本は「 KPT 」を軸に同じような内容で設定しました) 希望と懸念+信号機(5分:アイスブレイク兼、期待値の確認) KPT (15分:ふりかえりの本丸) TをEffort&Painで整理(5分:なるべく小さくすぐ始められるTryを集める) +/⊿でふりかえりのふりかえり(5分:次回へのフィードバック回収)  なお開発チームはフルリモートで業務を行っており、ふりかえりの実施もWeb会議です。色々考えて SharePoint 上に配置した PowerPoint でホワイトボード風のページを用意して、時折画面共有しながら時間中にOnlineで共同編集する作戦をとりました。意外と成立します。 2. 開発業務以外にも、現状認識のチェックやア イデア 出しに KPT やEffort&Pain等の フレームワーク を使ってみた  1を何度か製品開発の スクラム でやって気を良くした私は、マネージャーに相談してチームMTGでも「ふりかえり」チックなグループワークを試すことにしました。一般化しづらい内容なので具体的には書きませんが、「上半期のxxを振り返りつつ来年のyy 戦略を考える」みたいな内容にグループワークを持ち込むようなイメージです。  こちらについての気付きは後述しますが、成立こそしたものの課題もありという感じでした。 W:わかったこと(気付き)  ここからは、実際に「ふりかえり」に取り組んでみて、わかったことや気付きを何個か書いていきます。 基本は KPT 。しかし書き方や時間配分は基本を守ったほうが良い。  環境によって差はあるかもしれませんが、大抵の人がすんなり入ってくれるのは KPT かなと感じています。ただ人数にもよりますが、 書き方と時間配分は基本を崩さないほうがいい と感じています。基本というのは、この辺です。 KeepとProblemとTryを書くタイミングは分ける。 グループワークとしての KPT は他メンバーの書いた内容からも気付きを得てほしい。まとめてやると個人が持っている KPT の論理展開を書いてしまいがち。 Kでポジティブにスコープを振り返る時間をきちんと取る。 これが不十分だとPTが個人の反省会になりがち。 そしてKのシーケンスは多少共感や発見を重視する。「あ、こんないい事あったんだ」と思えたらしめたもので、「どうしたら他にも良くできただろう」という方向にいく 1 。  また KPT は最後のTを、きちんと明日からのTryに繋げていくのが一番難しいなと感じています。ついついPの議論は盛り上がりがちですが、Tもちゃんと時間を取って全員で整理しないと、せっかくメンバーが出してくれたTryを積み残しがちでした。この辺はまだ修行中です。Feasible & Useful(実現可能性と有用性の2軸でTryをプロットする)なども使いながら、全員で見える形にして実現性や優先度を整理すると改善に繋がりやすいかなと感じています。 ツールはなんだっていい。ただ、参加者にとって平等で、直感的に使いやすいことは大事。  全員がフルリモートの状況でふりかえりを始めようと思った時に、最初に迷ったのがツールでした。専用ツールでオサレにやりたい欲が頭をよぎったものの「 初回はとにかく説明なしで誰もがすぐ入れるもので 」と思い、弊社ではOffice 365が浸透していた事をふまえて、 PowerPoint を online編集する形を取りました。好みもあると思いますが、Office 365が利用できる環境であれば、手っ取り早い選択肢の一つとしてお勧めです。  結局この辺りは ファシリテーター が ファシリテーション しやすいかで決めていいと思いますが、同時に大事かなと感じているのは「 参加者にとって平等で、直感的に使いやすい事 」だと思っています。例えば KPT を個人で書き出すだけなら書き下しのメモ帳やチャットツールのスレッドでも可能ですが、グループワークとしてのふりかえりでは各人のアイテムがなるべく平等に並んでいる状態が理想です。テキストの書き下しはどうしても順序性に縛られてしまい、オンラインな議論よりも バッチ処理 的な読み下しになりがちです。  この点ホワイトボードツールに類するものは、位置を移動させる事で簡単に近似性や階層構造をその場で表現できる(そして違っていたら戻せる)のが圧倒的に有利です。いかに会議室で模造紙に貼る付箋が要件を満たしていたかを再発見した気分でした。 アレンジする時は「何が目的か」「迷いなくスコープを見つめてもらえるか」を考えておくことが大事。  アレンジはまだあまりしていませんが 2 、目的が ファシリテーター の中でハッキリしていれば、アクティビティの多少の組み替えは機能しました。  ただ当たり前かもしれませんが、アレンジを組み込む時は、「この書き方で参加者全員が同じように受け取れるか?」よくよく考えて準備する必要もあります(ふりかえりに限った話ではないでしょうけど)。  少し恥ずかしい失敗例ですが、メンバーがグループワークに慣れてきたあたりで、ふりかえりに関係のない MTG でも自己流のグループワークを設定してみました。ところが参加者からは「これってどこまでを含みますか?」「○○は含まれますか?」という質問が頻発して認識合わせに意外と時間を使ってしまいました。自己流が全くダメとは言いませんが、こんな質問が参加者から頻発する場合は、 用意した枠組みが本当に抜け漏れ、重複のないものか  また 言葉の定義が参加者共通で認識できているか よく問い直す必要があると気付かされました。そしてふりかえりの既存手法は直感的にこの辺りをクリアしやすいよう簡潔になっているのだな、という事にも気付けました。 リモート ファシリテーション に「ふりかえり」は効く。  結局のところ自分がふりかえりに惹かれたのは 「どうすればリモートワークでも発散系 MTG を実りあるものにできるか」という問題意識に一番ミートした というのもあります。Web会議はどうしても同時発話が成立しづらかったり、同期的な議論がしづらい場面があって、その点決まったルールに従って非同期で参加者が個人の考えを書き出していくシーケンスを挟む方式は相性が良いと感じています。  以上、多少シチュエーション依存の内容になるため一般化しづらい内容もありますが、何かしらの参考になれば幸いです。 T:つぎやること  個人的に次やりたいことは色々あります。本記事を書きながら色々と調べていて、発端になったガイドブック作者の森さんが結構な量のリソースを公開していることに気付きました。ある程度自分でTryした後に、こうした外部情報でノウハウ面を 仕入 れ続けるのは効果が高そうだと感じています。 Qiita:ふりかえりを拡張する「ふりかえりカタログ」   後は何冊か他の本にも潜ってみようかなと思います。 アジャイルレトロスペクティブズ 強いチームを育てる「ふりかえり」の手引き  また(自戒を込めてですが) 「ふりかえりって結局何のためにやるの?」 という問いへの答えは常に考えておきたいところです。この答えはおそらく、ふりかえりを実施する場面や人、チームによって様々だと思います。この点は、最後に少しだけ自分たちの価値観の話をします。 ユニコーン企業のひみつ  今年チームで読んだ書籍に「 ユニコーン 企業のひみつ」という書籍があります(素晴らしい本です)。この書籍の好きな一節に 「スタートアップの本質は『学習する機械』であること」 という言葉があります。  私達はスタートアップでこそありませんが、チームとして常に学習すること、成長することを重視しています。ふりかえりの目的や捉え方は様々かと思いますが、私は「チームが学習する機械であるために有用なアクティビティの1つ」だと捉えています。アクティビティとしてのふりかえりは楽しみつつ、この辺りの価値観を体現できるよう「チームで柔軟に試し続ける・変え続ける」 姿勢を今後もセットで大事にしたいと考えています。  皆様も是非、よきふりかえりライフを。 執筆: @fhiroaki 、レビュー: @sato.taichi ( Shodo で執筆されました ) 同じような話が及川さんの tweet にも出ていて、あぁウチだけじゃなかったんだなぁと。以下まとめは現場の知見がいっぱい集まってるのでおすすめです。 Togetterリンク:KPT (Keep/Problem/Try) 等のふりかえりで、K (Keep) ネタを増やすみんなの工夫 ↩ 「KとPは事前に書いて読み上げの時間は取らず、当日は他のメンバのものをじっと読んでレスポンスする時間にする」(メンバが発話に慣れて来たこと前提で、記述量が増えてきた後の時間短縮として)「KをFUN/DONE/LEARN に置き換えてそのままPTに行く」(メンバの成果の承認に時間を取りたい)くらいでしょうか。 ↩
 こんにちは。ISID XI 本部 AI トランスフォーメーションセンター所属の福竹です。  本記事は ISIDアドベントカレンダー記事 の7日目の記事となります。 これは何?(TL;DR) Why ふりかえり?(ふりかえりに興味を持つまで) What is ふりかえり?(ふりかえりって何?) Y:やったこと(やっていること) 1. きちんと時間をとって、開発スプリント毎のふりかえり(スプリントレトロスペクティブ)をやる(やった) 2. 開発業務以外にも、現状認識のチェックやアイデア出しにKPTやEffort&Pain等のフレームワークを使ってみた W:わかったこと(気付き) 基本はKPT。しかし書き方や時間配分は基本を守ったほうが良い。 ツールはなんだっていい。ただ、参加者にとって平等で、直感的に使いやすいことは大事。 アレンジする時は「何が目的か」「迷いなくスコープを見つめてもらえるか」を考えておくことが大事。 リモートファシリテーションに「ふりかえり」は効く。 T:つぎやること これは何?(TL;DR)  ふりかえり初心者が「ふりかえり」に興味を持ち、今年やって試したこと考えたことのふりかえりです。色々書きますが、コアメッセージは一つ「ふりかえりはいいぞ」に尽きます。 Why ふりかえり?(ふりかえりに興味を持つまで)   私の所属する組織 では、内製& スクラム でプロダクト開発をしています。10人弱の開発チーム全員が何らかプロダクトの開発者であり スクラム メンバーというチーム構成です。筆者個人は今年この開発チームにジョインし、現在はプロダクトの1つの スクラム マスター(修行中)を務めています。  ……とは言ったものの、私自身 スクラム 開発は現職が初めての経験で、個人的に本年はキャッチアップと試行錯誤の年でした。少しでもベース知識を 仕入 れようと スクラム 関連の書籍を読み漁っていたのですが、そんな折に誘われた社内の読書会(※仕掛け人はアドカレ6日目の橋詰さん)で本記事を書くきっかけの一冊に行き当たります。 アジャイルなチームをつくる ふりかえりガイドブック 始め方・ふりかえりの型・手法・マインドセット  「ふりかえり」という手法が色々と琴線に触れた事もあり、本書片手にチームメンバーの協力をいただきながら試行錯誤した事が、本日の本記事のネタになります。 What is ふりかえり?(ふりかえりって何?)  本記事はHow toを主眼にしないので詳しくは割愛させてください。具体感としては、「YWT」「 KPT 」「FUN/DONE/LEARN」等の、 アジャイル レトロスペクティブに端を発する(そして実際のところ アジャイル 開発関係なく活用できる)アクティビティを想定しています。詳しくは是非とも参考書籍&情報を御覧ください。 Y:やったこと(やっていること)  やったことそのものはシンプルです。 1. きちんと時間をとって、開発スプリント毎のふりかえり(スプリントレトロスペクティブ)をやる(やった)  最初に気付いた事は、担当製品のチームで既存メンバーが忙しすぎて、数ヶ月ほどふりかえり(スプリントレトロスペクティブ)が止まっていた事でした。そこで開発の山を超えたあたりで私が無邪気に「ふりかえりやりたい!」と言い出して、月1程度の頻度で、ふりかえりを設定して実施することにしました。(そして言い出しっぺの権限で好きに アジェンダ を組ませて貰いました)。基本的な事ですが、けっこう状況としてはあるあるだと思います。  とはいえ最初は右も左も分からないので、「ふりかえりガイドブック」付属の「 チートシート 」を参考に恐る恐る アジェンダ を組みました。以後も基本的には一部を組み替えつつ、 チートシート を参考にしています。拙くて恐縮ですが、 アジェンダ 例(45分)はこんな感じです。私がそうだったのですが、やりはじめは具体例を知りたくなると思うので載せておきます。(2回目以降も、基本は「 KPT 」を軸に同じような内容で設定しました) 希望と懸念+信号機(5分:アイスブレイク兼、期待値の確認) KPT (15分:ふりかえりの本丸) TをEffort&Painで整理(5分:なるべく小さくすぐ始められるTryを集める) +/⊿でふりかえりのふりかえり(5分:次回へのフィードバック回収)  なお開発チームはフルリモートで業務を行っており、ふりかえりの実施もWeb会議です。色々考えて SharePoint 上に配置した PowerPoint でホワイトボード風のページを用意して、時折画面共有しながら時間中にOnlineで共同編集する作戦をとりました。意外と成立します。 2. 開発業務以外にも、現状認識のチェックやア イデア 出しに KPT やEffort&Pain等の フレームワーク を使ってみた  1を何度か製品開発の スクラム でやって気を良くした私は、マネージャーに相談してチームMTGでも「ふりかえり」チックなグループワークを試すことにしました。一般化しづらい内容なので具体的には書きませんが、「上半期のxxを振り返りつつ来年のyy 戦略を考える」みたいな内容にグループワークを持ち込むようなイメージです。  こちらについての気付きは後述しますが、成立こそしたものの課題もありという感じでした。 W:わかったこと(気付き)  ここからは、実際に「ふりかえり」に取り組んでみて、わかったことや気付きを何個か書いていきます。 基本は KPT 。しかし書き方や時間配分は基本を守ったほうが良い。  環境によって差はあるかもしれませんが、大抵の人がすんなり入ってくれるのは KPT かなと感じています。ただ人数にもよりますが、 書き方と時間配分は基本を崩さないほうがいい と感じています。基本というのは、この辺です。 KeepとProblemとTryを書くタイミングは分ける。 グループワークとしての KPT は他メンバーの書いた内容からも気付きを得てほしい。まとめてやると個人が持っている KPT の論理展開を書いてしまいがち。 Kでポジティブにスコープを振り返る時間をきちんと取る。 これが不十分だとPTが個人の反省会になりがち。 そしてKのシーケンスは多少共感や発見を重視する。「あ、こんないい事あったんだ」と思えたらしめたもので、「どうしたら他にも良くできただろう」という方向にいく 1 。  また KPT は最後のTを、きちんと明日からのTryに繋げていくのが一番難しいなと感じています。ついついPの議論は盛り上がりがちですが、Tもちゃんと時間を取って全員で整理しないと、せっかくメンバーが出してくれたTryを積み残しがちでした。この辺はまだ修行中です。Feasible & Useful(実現可能性と有用性の2軸でTryをプロットする)なども使いながら、全員で見える形にして実現性や優先度を整理すると改善に繋がりやすいかなと感じています。 ツールはなんだっていい。ただ、参加者にとって平等で、直感的に使いやすいことは大事。  全員がフルリモートの状況でふりかえりを始めようと思った時に、最初に迷ったのがツールでした。専用ツールでオサレにやりたい欲が頭をよぎったものの「 初回はとにかく説明なしで誰もがすぐ入れるもので 」と思い、弊社ではOffice 365が浸透していた事をふまえて、 PowerPoint を online編集する形を取りました。好みもあると思いますが、Office 365が利用できる環境であれば、手っ取り早い選択肢の一つとしてお勧めです。  結局この辺りは ファシリテーター が ファシリテーション しやすいかで決めていいと思いますが、同時に大事かなと感じているのは「 参加者にとって平等で、直感的に使いやすい事 」だと思っています。例えば KPT を個人で書き出すだけなら書き下しのメモ帳やチャットツールのスレッドでも可能ですが、グループワークとしてのふりかえりでは各人のアイテムがなるべく平等に並んでいる状態が理想です。テキストの書き下しはどうしても順序性に縛られてしまい、オンラインな議論よりも バッチ処理 的な読み下しになりがちです。  この点ホワイトボードツールに類するものは、位置を移動させる事で簡単に近似性や階層構造をその場で表現できる(そして違っていたら戻せる)のが圧倒的に有利です。いかに会議室で模造紙に貼る付箋が要件を満たしていたかを再発見した気分でした。 アレンジする時は「何が目的か」「迷いなくスコープを見つめてもらえるか」を考えておくことが大事。  アレンジはまだあまりしていませんが 2 、目的が ファシリテーター の中でハッキリしていれば、アクティビティの多少の組み替えは機能しました。  ただ当たり前かもしれませんが、アレンジを組み込む時は、「この書き方で参加者全員が同じように受け取れるか?」よくよく考えて準備する必要もあります(ふりかえりに限った話ではないでしょうけど)。  少し恥ずかしい失敗例ですが、メンバーがグループワークに慣れてきたあたりで、ふりかえりに関係のない MTG でも自己流のグループワークを設定してみました。ところが参加者からは「これってどこまでを含みますか?」「○○は含まれますか?」という質問が頻発して認識合わせに意外と時間を使ってしまいました。自己流が全くダメとは言いませんが、こんな質問が参加者から頻発する場合は、 用意した枠組みが本当に抜け漏れ、重複のないものか  また 言葉の定義が参加者共通で認識できているか よく問い直す必要があると気付かされました。そしてふりかえりの既存手法は直感的にこの辺りをクリアしやすいよう簡潔になっているのだな、という事にも気付けました。 リモート ファシリテーション に「ふりかえり」は効く。  結局のところ自分がふりかえりに惹かれたのは 「どうすればリモートワークでも発散系 MTG を実りあるものにできるか」という問題意識に一番ミートした というのもあります。Web会議はどうしても同時発話が成立しづらかったり、同期的な議論がしづらい場面があって、その点決まったルールに従って非同期で参加者が個人の考えを書き出していくシーケンスを挟む方式は相性が良いと感じています。  以上、多少シチュエーション依存の内容になるため一般化しづらい内容もありますが、何かしらの参考になれば幸いです。 T:つぎやること  個人的に次やりたいことは色々あります。本記事を書きながら色々と調べていて、発端になったガイドブック作者の森さんが結構な量のリソースを公開していることに気付きました。ある程度自分でTryした後に、こうした外部情報でノウハウ面を 仕入 れ続けるのは効果が高そうだと感じています。 Qiita:ふりかえりを拡張する「ふりかえりカタログ」   後は何冊か他の本にも潜ってみようかなと思います。 アジャイルレトロスペクティブズ 強いチームを育てる「ふりかえり」の手引き  また(自戒を込めてですが) 「ふりかえりって結局何のためにやるの?」 という問いへの答えは常に考えておきたいところです。この答えはおそらく、ふりかえりを実施する場面や人、チームによって様々だと思います。この点は、最後に少しだけ自分たちの価値観の話をします。 ユニコーン企業のひみつ  今年チームで読んだ書籍に「 ユニコーン 企業のひみつ」という書籍があります(素晴らしい本です)。この書籍の好きな一節に 「スタートアップの本質は『学習する機械』であること」 という言葉があります。  私達はスタートアップでこそありませんが、チームとして常に学習すること、成長することを重視しています。ふりかえりの目的や捉え方は様々かと思いますが、私は「チームが学習する機械であるために有用なアクティビティの1つ」だと捉えています。アクティビティとしてのふりかえりは楽しみつつ、この辺りの価値観を体現できるよう「チームで柔軟に試し続ける・変え続ける」 姿勢を今後もセットで大事にしたいと考えています。  皆様も是非、よきふりかえりライフを。 執筆: @fhiroaki 、レビュー: @sato.taichi ( Shodo で執筆されました ) 同じような話が及川さんの tweet にも出ていて、あぁウチだけじゃなかったんだなぁと。以下まとめは現場の知見がいっぱい集まってるのでおすすめです。 Togetterリンク:KPT (Keep/Problem/Try) 等のふりかえりで、K (Keep) ネタを増やすみんなの工夫 ↩ 「KとPは事前に書いて読み上げの時間は取らず、当日は他のメンバのものをじっと読んでレスポンスする時間にする」(メンバが発話に慣れて来たこと前提で、記述量が増えてきた後の時間短縮として)「KをFUN/DONE/LEARN に置き換えてそのままPTに行く」(メンバの成果の承認に時間を取りたい)くらいでしょうか。 ↩
金融ソリューション事業部の橋詰です、こんにちは。 このポストは 電通国際情報サービス 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 で執筆されました )