プログラマとしてPower Automateでローコード開発した所感

2020年10月15日 木曜日


【この記事を書いた人】
山本 悠滋

日本Haskellユーザーグループ(愛称 Haskell-jp)発起人の一人にして、Haskell-jpで一番のおしゃべり。 HaskellとWebAssemblyとプリキュアとポムポムプリンをこよなく愛する。

「プログラマとしてPower Automateでローコード開発した所感」のイメージ

こんにちは。IIJ-IIの山本悠滋です。普段はIIJ-IIの技術開発室という部署で、IIJ本体をサポートするための開発をいろいろしています。
今回は、先月Microsoft Power Automateというサービスで開発したプログラムと、開発して学んだことを共有したいと思います。

Power Automateとは

Microsoft Power Automate(旧「Microsoft Flow」)とは、一言で言うとIFTTTZapierのMicrosoft版、といったところです。「○○のウェブサービスで××というイベントが発生したら、その時の情報を△△という別のウェブサービスに送る」といった処理の自動化をブラウザ上でポチポチと設定するだけで実装できるツールです。さすがにMicrosoft製品だけあってか他のMicrosoft製品との連携機能が充実しているほか、無償版のIFTTTやZapierにはない、変数の操作や、ループなどの制御構造といった機能もあります。そうした、プログラミングにおいて重要なコンセプトを学習するツールとしてもよいかも知れません。他にも、プレミアム版を契約すればGUI操作の自動化(いわゆるRPA)をしたり、Common Data Serviceという専用のデータストアを使うことができるようになっています。本気で使い込めば結構本格的なアプリケーションが作れそうですね!

Power Automateの使用例

ここでPower Automateがどんなものかを皆さんによりわかりやすく実感していただくために、Power Automateを使って試しに簡単なフロー(Power Automateで作ったプログラムは「フロー」と呼ばれています)の作成例を紹介しよう … と思ったのですが、よく検索してみたらすでに、弊社ブログにわかりやすい例が書かれているではありませんか!

スクリーンショットが多く必要で長くなりそうですし、この記事に書くには大きすぎると思っていたところなので助かりました!ありがたく引用して、使用例の紹介に代えたいと思います?!

作った背景

IIJでは近年、現場から課題やアイディアを集めて、事業につなげたり現場の業務改善に活かしたりする取り組みを盛んに行っています。例えば、参加者が日頃温めているネタ・仮説について雑談してアイディアを生み出すことを目指す座談会が開催されたり、生み出したアイディアの提案を受け付けて事業化するところまでをサポートする企画が最近始まったりしました。

そうした取り組みによって発案され、検証された仮説を元に、今回はPower Automateを使って手早く目的を達成するプログラムを作りました。「Outlookのカレンダーに含まれるイベントのうち、とある条件に合致したものを選んでTeamsで通知する」という要件であった関係上、OutlookのカレンダーやTeamsとの連携がほとんどの処理を占めていたため、Power Automateのようなツールを使った方が簡単だろうと考えたのです。

使った感想

Power Automateのおかげで、今回私は2週間足らずで目的のフローを実装できました。この手のサービス全般に言えることとは思いますが、各種アカウントとの接続設定や、外部サービスが提供するAPIの使い方を理解する、といった仕事に労力を割かずに済んだ、という要因が開発期間の短縮に寄与したのではないでしょうか。以前別件でTeamsのbotを開発した際は、Microsoftアカウント周りで少し特殊な取り回しをする必要があり、その方法を調査・実装するだけで1ヶ月もかかってしまいました。それを思えば、その効果は絶大だったと言えるでしょう。「重要なことに時間をかけて その他のことは自動化しましょう」という現在のキャッチコピーは伊達ではないですね。

一方、ある程度複雑なことをしようとすると、途端に様々な問題にハマりました。なまじ変数の操作や制御構造など、この手のサービスで通常やらなさそうなことができてしまうだけに、それらを活用して込み入ったことに取り組むと、たちまち扱いづらさが見えてきます。「重要なことに時間をかけて その他のことは自動化しましょう」の「重要なこと」に集中させてくれるまではいいのですが、その「重要なこと」をPower Automateだけで実装するのが中々ハードなのです…(プレミアム版だと任意のREST APIを実行する機能もあるそうなので、複雑なことは自前のサーバーに任せるのも一案かもしれません)

そこでこの節では、私が今回のフロー作りで直面したPower Automateの問題と、その対策を共有したいと思います。今後Power Automateでうっかり複雑なフローを作ることになってしまった方の参考になれば幸いです。

ODataのクエリーだったり、特別な構文の式を直接書かないといけない場面がある

例えば、やや強引な例ですが「Outlookのカレンダーにあるイベントのうち、件名に自分の名前が含まれているイベント」を取得するフローは以下のようなものです。

「自分の名前」を「マイプロフィールの取得 (V2)」というステップで取得した後、「カレンダーの取得 (V2)」・「イベントの取得 (V4)」で自分のカレンダーに記録されているすべてのイベントを取得し、「アレイのフィルター処理」で該当するイベントを選び出す

「自分の名前」を「マイプロフィールの取得 (V2)」というステップで取得した後、「カレンダーの取得 (V2)」・「イベントの取得 (V4)」で自分のカレンダーに記録されているすべてのイベントを取得し、「アレイのフィルター処理」で該当するイベントを選び出す、というフローです。

(配列のことを「アレイ」、選び出す対象の配列を「差出人」などと変な和訳をしているのはさておき)多くの場合、これで問題なく動くでしょう。ただし、このフローは「イベントの取得 (V4)」において、設定したカレンダーに含まれるすべての予定を取得してしまうため、ちょっと効率が悪いです。実際の上限がどれくらいかはわかりませんが、どうやら(定期的なイベントは除く)現在・過去・未来のすべての予定を取得するようなので、営業部門の方など毎日多くの予定を抱える人のカレンダーであれば、かなり時間がかかってしまうのではないでしょうか?

そうした場合は、「イベントの取得 (V4)」というステップで、「OData」におけるクエリ言語$filterというパラメータを使用します。「OData」はREST APIにおけるパラメータやレスポンスなどの仕様を統一するためのプロトコルです。REST APIのクエリパラメータで、SQLのSELECT文のようなAPIを表現するための仕様などを定めています。$filterパラメータは、要するにSQLで言うところのWHERE句に相当する式を与えるパラメータです。取得したいデータの条件を指定するパラメータですね。

このODataのクエリ、Outlookのカレンダーを取得するAPIを含め、Power Automateから呼び出せるREST APIにおいてしばしば利用できます。なので例えば今回挙げた例のように、「件名」に指定した文字列が含まれているイベントのみを取得する場合は、先ほどのスクリーンショットにおける「イベントの取得 (V4)」をクリックして「フィルタークエリ」という欄に下記のようなクエリを入力すればよいでしょう(詳しい構文は割愛します)

contains(subject, '「文字列」')

そして、この「文字列」の箇所を「マイプロフィールの取得 (V2)」で取得した自分の名前に置き換えるには、上記のODataのクエリを書いた「フィルタークエリ」という欄で、画面右に出ている「動的なコンテンツ」というポップアップから「名」を探して選択します。

入力欄右に出てくるポップアップから、「動的なコンテンツ」として「名」を探す

正しく「名」を挿入できれば、次のように二つのシングルクォートの間に「名」が表示されているはずです。できていなければ調整しましょう。

二つのシングルクォートの間に「名」が挿入された状態

これでひとまず、多くのケースでは意図通り「件名に自分の名前が含まれているイベント」を選び出せるはずです。しかしこれによって、新しい問題ができてしまいました。どんな問題でしょうか?ヒントは「SQLインジェクション」。

… そう、埋め込む文字列に対して適切なクォートをしなければSQLインジェクションが発生するのと同様に、この「フィルタークエリ」に書かれたODataのクエリは、適切なクォートができていないので、「名」にシングルクォート ' などの文字が含まれていた場合に、意図しない挙動となる恐れがあります。例えば、名が「O’Reilly」のようなシングルクォートを含む人がいれば、フローの実行時に「Invalid filter clause」というエラーが発生しますし、もっと悪意を持った「’ or 1 eq 1」なんて名の人がいれば、フローの作者が意図しないイベントを取得することになってしまうでしょう(フローを実行したユーザー自身のカレンダーを探る限り、問題になることはほぼあり得ないですが)。「フィルタークエリ」にはあくまでも条件を指定するための式しか入れることができないので、かの「Exploits of a Mom」のようにテーブルを消し去ってしまうほどのリスクはありませんが、厳密にやるならこの問題も直しましょう。

と、いうわけで「名」にシングルクォートが含まれていても問題が起きないようにするために、「名」に含まれるシングルクォートをエスケープしましょう。そのためには、「contains(subject, '名')」における「」の箇所をクリックして、先ほども出した「動的なコンテンツ」というボタンの右にある「式」というボタンをクリックし、以下の式を入力します:

replace(outputs('マイ_プロフィールの取得_(V2)')?['body/givenName'], '''', '''''')

これは、Workflow Definition Languageという形式のJSONに埋め込める、専用の式です。ちょっとしたプログラミング言語になっていますExcel風のif関数があるので分岐はできるものの、再帰呼び出しやループができないからチューリング完全ではなさそうですが)

上記?の式ではreplace関数を使ってシングルクォートをODataのフィルタークエリ向けにエスケープしています。
ODataの仕様曰く、フィルタークエリにおける文字列リテラルはシングルクォートで囲わなければならず、シングルクォートをエスケープするにはシングルクォートを二つ重ねる、という構文となっています。その上Workflow Definition Languageに埋め込む式も同様にシングルクォートの文字列リテラルでシングルクォートを二つ重ねてエスケープする、という仕様のため、上記のような難解な式を書くことになります?。

さて、ここまで長々とODataのクエリをPower Automateで入力する方法を解説しましたが、面倒くさいですよね…?。試しにここで紹介した手順をたどるとわかるとおり「動的なコンテンツ」の「式」を入力する欄が狭かったり、式を挿入する処理などに細かいバグがあったりして、とても煩雑です。その上、replaceまで入力した式は、下?の画像のように中身が見えないので一覧性が非常に悪くなります。

replace(...)

そんなわけで、この問題に直面しないためにもODataのクエリなど、特別な構文の式を書くのは可能な限り避け、富豪的なプログラミングを心がけましょう。そもそもここで挙げた例自体やや人工的ですし、最初に挙げた「アレイのフィルター処理」を使った方法でも大抵問題ないはずです。ローコードなプログラミングツールはコードをできるだけ書かないためのツールなのですから、書かない方が望ましいのはしかるべきことです。

私はプログラミングができてしまう、しかも別件でODataのクエリの書き方を学習していたため、ついつい書きたくなってしまったのです。それが失敗でした。ひとまず「アレイのフィルター処理」を使って実装して、様子を見るべきだったのです。

ただこれ以外にも、JSONを直接入力するステップを作成しなければならない場合もあるので、そういうやむを得ない時は歯を食いしばって入力して、必要ならちゃんとエスケープもしましょう?。今回の場合、私はTeamsに投稿するメッセージを作成するためにAdaptive Cardという形式のJSONを書きました。これもひょっとしたらAdaptive Cardを使わない、単純なメッセージの組み合わせで回避できたかも知れません。

対策: そもそもJSONやODataのクエリなどの、特別な構文の式を直接書かなくても良いよう、ローコードツールらしくマウスポチポチしましょう。

エラーが発生しているフローは保存できない

もう一つは例えば、参照している変数が定義されていない、というエラーがあるフローは保存できない、というものです。普通のプログラミング言語で例えるならこれは、構文エラーや型エラーなどのコンパイルエラーが出ている間はコードを保存できない、という問題です。

プログラミングをある程度経験している方なら、これがいかに厳しい制限かわかるでしょう。後でも触れますがエラーメッセージが一見してわかりづらい中、直せない限り中途半端な状態では保存できないのです。しかも、エラーがない状況になったら自動で保存してくれるわけでもないので、エラーにハマったら「直す」か「最後に保存したときまで戻る」かの二択しかないのです!どこぞのRPGの戦闘じゃあるまいし…。私はこの挙動のせいで2回ほど残業が増えました?。

この問題、実は先の節で紹介した「動的なコンテンツ」の「式」を記述している場合に、一段と解決を面倒にする恐れがあります。先ほど紹介した式を思い出してください:

replace(outputs('マイ_プロフィールの取得_(V2)')?['body/givenName'], '''', '''''')

この式におけるoutputs('マイ_プロフィールの取得_(V2)')は、「マイプロフィールの取得 (V2)」というステップの実行結果を返す式です。outputsという関数にステップの名前を渡すと、ステップの実行結果を取得できるんですね。続く?['body/givenName']という式で、「マイプロフィールの取得 (V2)」の結果から名を抽出しているのです。

ここで出てくる「ステップの名前」は、ステップの右上にある「…」というメニューボタンから自由に変更することができます。以下が実際に「マイプロフィールの取得 (V2)」の名前を「ユーザーの名が必要なのでプロフィールを取得」に変更したときのスクリーンキャプチャです:

「…」というメニューボタンをクリックして、「名前の変更」を選択する

ステップの名前を変更することで、ステップの意図を簡単に表明できます。ステップにコメントを書く機能は別途あるのでそちらに意図を書いてもよいのですが、ステップの名前を変えれば、フロー全体を見るときにも閲覧できるというメリットがあります。ステップがたくさんあるフローで適切に名前を付ければ、メンテナンスの際優秀な道しるべとなるでしょう。

しかし、このステップの名前を変更する機能と「動的なコンテンツ」の「式」が組み合わさったとき、落とし穴が現れます。「動的なコンテンツ」の「式」でoutputs関数を使った複雑な式を記述した場合、outputs関数に渡したステップの名前が、参照しているステップの名前を変更しても反映されないのです。結果、画面右上の「フローチェッカー」に、次のようなエラーが表示され、保存できなくなってしまいます。

アクション ‘イベントの取得_(V4)’ の入力パラメーターに対して、‘マイ_プロフィールの取得_(V2)’ への有効な参照を含めるには、直してください。

普通のプログラミングで例えるならこの状況は「変数や関数の名前を変えたけど、それを参照していた箇所が変更に追いついていない」という日常茶飯事な状況です。普通のプログラミング言語で書かれたテキストファイルであれば一括置換で済みますし、修正中でも保存できます。しかし、Power Automateでは一括置換も修正中の保存もできません!結果、エラーが発生した箇所を一つ一つ探してポチポチクリックしては書き換えるしかないのです。やはり複雑な式は入力しないに越したことはありません。

ちなみに、「動的なコンテンツ」の「式」機能ではなく、「動的なコンテンツ」の下に出てくる項目の一覧からクリックして挿入した場合(例えば下記?のように表示される項目)は、ステップの名前変更にちゃんと追従します。「式」機能を使わない方がいい理由が増えましたね。

名

対策:

  • こまめに保存しましょう。基本的にはそれしかありません。本当にゲームみたいですね…
  • ステップの名前を変更する場合、特に「動的なコンテンツ」の「式」で参照する場合には、早めに名前を決めましょう
    • 繰り返しになりますが、そもそも「動的なコンテンツ」の「式」を使うのはなるべく控えましょう
  • あるいは、この問題を修正してほしいという提案が下記の通り3件もあるので、「Vote」して祈りましょう?

変更履歴が一切追えない

掲題のとおりです。Power Automateには変更履歴はおろか、単純なアンドゥ(「元に戻す」機能)すらありません。Gitなどのバージョン管理システムに慣れている人なら、いや、そうでなくとも「元に戻す」機能を何度か使ったことがある人なら、これがいかに不便かピンとくるでしょう。昔のバージョンに気軽に戻せないということは、何か問題が発生したとき、正常に動いていたバージョンに戻したり、正常に動いていたバージョンと比較する、といったことが難しくなるため、変更に伴うリスクと心理的なハードルがグッと高まります。

幸い、その点をMicrosoftも意識しているのか、「エクスポート」機能を使うと、保存できている状態のフローをzipファイルに固めてダウンロードできます。しかも、ダウンロードされるファイル名にはデフォルトでタイムスタンプがつきますし、エクスポートする画面でコメントを入力することもできます。「ファイル名 + タイムスタンプ」という古典的なバージョン管理方法を推奨しているわけですね?。自分で新しい名前を都度考えて「完成版」「完成版 その2」「今度こそ本当の完成版」みたいな名前のファイルを大量に作ってしまうよりはマシでしょう。

対策: こまめに保存・エクスポートしよう

オフにしているフローは手動でさえ実行できない

Power Automateで作成したフローは、フローの詳細画面から「オフにする」ことができます。自動で実行しないよう設定できるわけですね。この機能自体はありがたいのですが、残念ながら「オフ」にしている間はテストのための実行すらできなくなる、という痛い副作用があります。

それでどういう場合に困るのかというと、例えばフローを「10分おきに実行する」フローにした場合です。普通Power Automateで作るフローは、接続するサービスに対して何らかの副作用、つまりデータの書き換えや追記を発生させるものです。テストのために手動で実行しようと「オン」にしたら知らない間に実行されてしまい、実行履歴に余計な記録ができたり、最悪の場合データの不整合を生じさせてしまいます。

対策

エラーメッセージが極めてわかりづらい

続いての問題について、実際にわかりづらかった例を一つ挙げようと試みたのですが、最初に件のエラーに出くわしたときからエラーメッセージが変わったのか、記憶と違って少しわかりやすくなっていたり、他のエラーメッセージについては再現方法が思い出せなくなっていたりしているので、具体例は割愛します?。

とは言え、作っているうちに多かれ少なかれ理解できないエラーメッセージに遭遇することはあるでしょう。Power Automateは非常に複雑なアプリケーションです。エラーが発生する要因はそこら中に潜んでいます。いかにMicrosoftといえど、エラーをうまくハンドリングし損ねてしまい、ユーザーにとって難解なエラーとして表示させてしまうことは珍しくありません。

対策

そうした事態における窓口が、こちらのPower Automateのサポートページです。その中でも、エラーでハマったときに最も役立つのはやはり公式のコミュニティでしょう。エラーメッセージで検索したときにも、しばしばコミュニティのスレッドがヒットします。

ただし、この手のコミュニティにありがちなことですが、日本語でのやりとりは一切できません…?。英語に自信がない場合はGoogle翻訳なりDeepLなりを駆使して根気強く助けを求めましょう?。

そのほか、投稿して助けを求める場合は以下の点に注意してください:

  • ソースコードをコピペして見てもらう、ということができないのでスクリーンショットを貼り付けることになります。このとき画面表示が日本語だと当然サポートする人も読めないでしょうから、あらかじめ表示言語は英語にしておいた方がいいでしょう
    • できれば、SharePointのリストにおける列の名前も英語??に訳しましょう。こちらは表示言語を変えるだけでは変わらないので、頑張って訳しましょう…
  • Power Automateに限らない、投稿する以前の事項ですが、事前にこまめに検索して、自分が悩んでいる問題がすでに解決済みの問題でないかチェックしておきましょう
    • 私は幸い今回は過去のスレッドを検索するだけで事足りました?。きっとみなさんも、丹念に検索すれば見つかるでしょう
  • これもPower Automateに限らない事項ですが、Power Automateのコミュニティはよくある、主にユーザー同士が助け合う掲示板です。技術系メーリングリストで質問するときのパターン・ランゲージなどを参考に、マナーを守った質問を心がけてください

なお、日本語でサポートを得たいという場合は、プレミアム版を契約すればMicrosoftからサポートを得られるようです。

おわりに

Power Automateは、単純な「ただサービスAとサービスBをつなぐだけ」みたいなプログラムには大変便利ですが、ある程度の複雑さを求めると途端にしんどくなります。ここまで挙げたような問題にぶち当たり出すと、プログラミング経験のない人には中々手に負えないのではないでしょうか。それこそ、IFTTTやZapierも得意であろう極めて単純なフローならばともかく、それ以上の用途に使うにはまだまだ発展途上だなぁと言う印象です。こうしたツールが使いやすくなって、プログラミング専門じゃない人がもっとたくさん自動化に取り組めるような未来が来てほしいものですね。

山本 悠滋

2020年10月15日 木曜日

日本Haskellユーザーグループ(愛称 Haskell-jp)発起人の一人にして、Haskell-jpで一番のおしゃべり。 HaskellとWebAssemblyとプリキュアとポムポムプリンをこよなく愛する。

Related
関連記事