これまではエンジニア個人の技術的関心を中心に記事を公開してきました。
続きを読む...
The post TECHSCORE BLOG をリニューアルしました first appeared on TECHSCORE BLOG.
]]>これまではエンジニア個人の技術的関心を中心に記事を公開してきました。今後はプロダクト開発にまつわる話をメインテーマとして、シナジーマーケティングのエンジニアリングとエンジニアの姿をお届けしていきます。
こちらの記事はアーカイブとして残し削除することはありません。今後は新しいブログにて記事の公開を行っていきます。
今後ともよろしくお願いいたします。
The post TECHSCORE BLOG をリニューアルしました first appeared on TECHSCORE BLOG.
]]>横田です。転職して3カ月程ですが前職までは Oracle や SQL Server を中心に触っていました。
続きを読む...
The post パフォーマンスを考慮したIndex定義設計 first appeared on TECHSCORE BLOG.
]]>横田です。転職して3カ月程ですが前職までは Oracle や SQL Server を中心に触っていました。本格的にPostgreSQLを使うようになったのが今回始めてのため、SQL Server の言葉を借りて説明している箇所が有ります。予めご了承ください。
今回は、PostgreSQL 11 で実装された付加列Index (※) を中心としたパフォーマンス関連のお話をしたいと思います。商用DBと比較してしまうとOSS-DBに無い機能が多かったりするのですが、近年はパーティショニングやIndex機能の充実化など、溝を埋めていこうとする姿に感銘を受けました。なのでDBを使う側も実装されたものを使いこなせる様になろうと思います。
※「付加列Index」はSQL serverで同機能の呼び名です。機能の詳細は後述しますが、PostgreSQLでは同機能に名前が無いので、この記事では便宜上この機能をPostgreSQLでも「付加列Index」と書いていきます。
今回のお話は「Indexって何だろう?」を卒業したところをスタートラインとしています。もしIndexについておさらいしたい方は以下のサイトを読んでみては如何でしょうか。まあ、Indexを簡単に言うと「検索を早くするためのもの」です。
→(参考)PostgreSQL公式サイトの説明(外部サイトに飛びます)
→(参考)B-treeインデックス入門(外部サイトに飛びます)
→(参考)Indexの種類について
まず最初に、B-Tree型Indexの基本的なアクセスパスについて以下のオブジェクトを使って説明します。
※SQL serverに読み替えて読んでいる方は、非クラスタ化インデックスを作成したと想定してください。
[ テーブル定義を表したDDL ] ※データは入っている前提でお話します。
CREATE TABLE TEST1 ( x int , y int , z int ) ;
CREATE INDEX TEST1_x1 ON TEST1( x ) ;
このテーブルに対して以下のクエリを発行したとします。
SELECT x , y FROM TEST1 WHERE x = 3 ;
この場合、以下の流れになります。(図2)
1)Indexを使って[ x = 3 ] の行を探して、実データテーブルのアドレスを特定します。
2)そのアドレスを使って実際のテーブル行からデータを取得します。
この1~2の流れでデータを探す動作は、SQL serverの言葉を使うとRID Lookup、Oracleの言葉を使うとTABLE ACCESS BY INDEX ROWID と言います。PostgreSQLでの呼び名が解らないので、本記事では便宜上この動作をRID Lookupと呼んで説明をしていきます。RID Lookupが発生すると余計な処理が発生して遅くなると思ってください。
(図2) B-Tree Indexの基本的なアクセスパス
先ほどのSQLではIndexのアクセスパスとしてRID Lookup(図3)が発生していたのですが、Indexスキャン時にノードの値を見れば x列の値は解るので、Indexに無いy列の情報だけ探しに行けば早くなるのかな?という発想が出てきます。
(図3)RID Lookupの流れ
ならばと言う事で、クエリのselect句で問い合わせている列を全部Indexに持たせてしまえば RID Lookupは発生しないのでは?という考え方から生まれたIndexをカバリングインデックスと呼びます。但し、何も考えずただ単にSelect 文で使う列を入れた複数列インデックス(※)を作れば良いのかというと決してそうではなく、カバリングインデックスが使用されて実行計画が Index-only-scan となるためには一定の条件が必要です。
※商用DBで 複合Index と言われているIndexのことです。PostgreSQLでは複数列インデックスと呼びます。
1.はPostgreSQLのサイトを確認してバージョンと型を確認してみてください。
2.は簡単な話で、最初に書いたIndexは x列だけに設定されているので、y列にもIndexを定義すれば良いだけです。つまり以下のIndex を作成すれば良いです。
CREATE INDEX TEST1_x1 ON TEST1( x , y ) ;
「Indexに格納されている列だけを使用する」という条件はそのままの意味で、カバリングインデックスが使用されるパターンは以下の通りです。
[使用する]
SELECT x FROM TEST1 WHERE x = 3 ;
SELECT x , y FROM TEST1 WHERE x = 3 and y = 3 ;
[使用しない]
SELECT x , y , z FROM TEST1 WHERE x = 3 ;
SELECT x , y FROM TEST1 WHERE x = 3 and z = 3 ;
なんだ、カバリングインデックスって簡単じゃないか。これでクエリが早くなるな。とほっこりした方は詰めが甘い。では、もう少しPostgreSQLのアーキテクチャを覗いてみましょう。
DBは複数のユーザー、複数のトランザクションが同時に様々な処理を行います。それらのトランザクションの一貫性やデータとしての整合性を担保しているのはロック機構だけではありません。MVCC(MultiVersion Concurrency Control:多版型同時実行制御)と言って、それぞれのトランザクション毎にスナップショットを用意し、それに対して操作を行っています。トランザクションが終わったらトランザクション隔離レベルで定めたルールに従って他のスナップショットと帳尻を合わせるという制御方式が取られています。その処理においてもカバリングインデックスのアクセスパスの無駄を省くよう考えておかないといけません。
→(参考)MVCC(MultiVersion Concurrency Control:多版型同時実行制御)
それを踏まえて、カバリングインデックスが最速で動くための付随条件を考えてみましょう。
※PostgreSQL以外で読み替えている方は1は考えなくても良いと思います。
PostgreSQLならではの仕様なので、少し説明します。PostgreSQLには可視性マップ(Visibility Map)という仕様があります。これはPostgreSQLが追記型アーキテクチャであるが故の仕様(例えばUPDATE文を実行すると元レコードを無効化したうえで、修正値で新しいレコードを追加するという仕様)のため、VACUUMする必要があるかどうかを判断するため、あるいはIndexを読んだ際にテーブルのレコード本体と一致しているか確認するために読みに行く必要があるかどうか、といった判断をする必要が有るために存在します。(図4)
(図4) 追記型アーキテクチャで起こる問題
可視性マップは、各ブロックのレコードの可視性状態を保持するファイルで、テーブルヒープの各ページ毎に可視/不可視の情報をビットマップで保持しています。(図5) 実態は拡張子 “_vm” のファイルで、ブロックをVACUUMする必要があるかどうかの判断とIndex-Only Scan(レコード本体を見ない)できるかどうかの判断に使います。
(図5) 可視性マップ(Visibility Map)の構造
全てのビットが立っていると、ブロック内の全タプルが全トランザクションに可視である(つまりIndexの情報と実データテーブルは一致している)事が解るので、テーブルファイルにアクセスしなくても、Indexで得たタプルのデータがそのまま使えることが解ります。(図6) パフォーマンス向上のためにも、可能な限りビットを立たせるようにしたほうが良いと思います。業務仕様で更新の多いテーブルでは他の施策を考えたほうが良いでしょう。
(図6) 可視性マップの使われ方
こういった無駄な動きをさせないようにするために、可視性マップのフラグを立ててください。というのが1.で言いたかったことです。実際のアクションとしては「VACUUM を実行する」で正解なのですが、” おまじない ” の様にするのではなく、内部動作を理解したうえで行うと「カバリングインデックスが有効になっているのに何故か遅い」という場面でも対処法が判るようになると思います。
複数列インデックスの特性はPostgreSQLの公式マニュアルにも記載されていますが、定義列の順番が重要で一番最初(一番左)に列を最初に検査します。WHERE句で一番最初の列を使用しなかったりすると、プランナが Table Scan のほうが早いと判断してしまう傾向があるようなので、Index設計はプログラム設計のフェーズでしっかり吟味する必要があると思います。(図7)
(図7) 複数列インデックスの使われ方
一番賢い作り方としては、回答を返すだけ列(別にIndexとして機能しなくてもいい列)を末尾の列することだと公式マニュアルにも書いてあります。パフォーマンスを考慮したIndexの一例として書きましたが、上記を考慮してご自身の環境でもテーブル設計を見直しては如何でしょうか。
SQL serverではお馴染みの機能ですが、PostgreSQL 11より実装されました。Index scanだけでクエリの回答をしようという考え方はカバリングインデックスと同じですが、[Indexとして機能させたい列]と[回答を返す値だけを保持する列]とを分離させたIndexです。(図8)
(図8) 付加列インデックスの構造
作り方は簡単で[回答を返す値だけを保持する列]をINCLUDE句に指定すれば良いだけです。
[ サンプルDDL ]
CREATE INDEX TEST1_x1 ON TEST1( x ) INCLUDE( y , z );
[使用例]
SELECT x , y , z FROM TEST1 WHERE x = 3 ;
上記SQL を実行すると、TEST1_x1のリーフノードまで検索されて実データテーブルのアドレス”■”まで辿り着くのですが、そのリーフノードにInclude句で指定した列のアドレスではなく実値として格納されているので、可視性マップを見て全て「可視(ビットが立っている)」であれば、その値を使って結果を返します。 RID Lookupが発生せず高速なスキャンが実現できます。
INCLUDE句はPRIMARY KEYの制約として書くことも出来ます。便利ですが一定の制約もあります。INCLUDE句に含めるペイロード列に、インデックス型の最大サイズを超えるタプルを指定すると失敗します。また、INCLUDE句に列を含め過ぎてインデックスサイズが膨張すると、検索が遅くなるという元も子もない状態に陥ります。
また、カバリングインデックスの項でも触れましたが、Index scanの際に必ず可視性マップを確認しに行きますので、そこでフラグが立っていなければ、結局実データテーブルを見に行きますので Indexが肥大化している分、逆に遅くなる可能性があります。
複数列インデックスについても、4列を超えた複数列キー定義は問題があると公式サイトにも書いています。
CRUDのRとCUDを分離した分散構成でもない限り、クエリの高速化は更新処理の速度劣化とトレードオフの関係です。ただ、更新処理はストレージアクセスを先送りできる特性が有るので、クエリの高速化に偏重する考え方でも決して間違いではないのですが、システムの特性を鑑みたバランスが一番大切だと思います。要はDBを理解した上でのプログラム設計の重要性に帰結するのではないでしょうか。
DBを中心としたパフォーマンスチューニングを10年程経験していますが、前職、前々職の現場で「ある日突然遅くなったのでチューニングしてほしい」という依頼を度々受けたことがあります。統計情報が劣化した?IndexかTableの断片化が激しい?Indexが過多?不足?色々と調べることはありますが、原因究明にDBだけを見ている開発者が多いと感じます。プログラム側でトランザクションの設計は適切ですか?そのコミットタイミングやコミット間隔は適切ですか?システム設計時の想定処理件数に対して、今の件数で処理するとどうなりますか?設計時に運用上のデータ逓増をどれだけ設計に取り入れていますか?という質問に閉口されてもチューニングを依頼された側としては困ってしまいます。
DB内で更新データがどのように流れているのか、どこで管理されているのかを知っていれば、特にコミットタイミングやコミット間隔が適切でない事態は防げるかもしれません。今回はIndexのお話でしたが、機会があればパフォーマンスの劣化を防ぐ設計の考え方についてお話しできればと思います。
The post パフォーマンスを考慮したIndex定義設計 first appeared on TECHSCORE BLOG.
]]>テストチームを離れて数年、久しぶりに品質系の仕事に携わることになり、改めて「品質とは何か」を考えてみました。
続きを読む...
The post 品質とは first appeared on TECHSCORE BLOG.
]]>テストチームを離れて数年、久しぶりに品質系の仕事に携わることになり、改めて「品質とは何か」を考えてみました。
「品質とはなんぞや」と問われたら、どのような回答が返ってくるのでしょうか。
バグの発生度合?、信頼性の高低?、安全性の高低?、操作性の良し悪し?、お値段の高低?
どれも文脈によっては当てはまりそうです。
悩んでも答えは出そうにないので、規格の定義やその道の権威の方の考えに頼ることにします。
となっていました。
これらを見ると、品質とは「要求を満たしている度合」と「満足、価値を生み出している度合」という2つの考え方があるように見受けられますが、ISOなどの規格では要求の妥当性を確認するためのプロセスも定められているため、「満足、価値を生み出している度合」と考えられます。
品質が「満足、価値を生み出している度合」とすると、誰にとっての満足、価値を考えるべきなのでしょうか。
真っ先にお客さま(利用者)の存在が浮かびますが、他にも従業員、経営者、株主、取引先などのステークホルダーが存在します。
また、最も重要な指標であるお客さま(利用者)の満足、価値をどのような観点で整理するべきでしょうか。
東京理科大学名誉教授の狩野紀昭氏が提唱した「狩野モデル」を用いて整理するのが良さそうです。
これはお客さま(利用者)の満足、価値に影響を与える品質要素を以下のように分類し、優先度を整理するものです。
こうやって整理すると、お客さま(利用者)が気にしないポイントや逆に離反をまねくようなポイントに注力するリスクを軽減できますね。
この点をさらに掘り下げて考えたいところですが、それはまたいつか。
The post 品質とは first appeared on TECHSCORE BLOG.
]]>こんにちは。松本です。
続きを読む...
The post ITP 1.0 から 2.3+α までの解説 first appeared on TECHSCORE BLOG.
]]>これは TECHSCORE Advent Calendar 2019 の 23 日目の記事です。
こんにちは。松本です。
社内向けに作成した ITP(Intelligent Tracking Prevention) に関するドキュメントを再編集し、ブログ記事として以下に公開します。対象は、ITP 1.0 から ITP 2.3 に加え、2019/12/10 に発表された Preventing Tracking Prevention Tracking までとなります。
WebKit のソースコードに目を通すところまでは出来ていないので、あくまでも WebKit ブログから読み解ける内容が中心となります。
まず、ITP に関連して頻出する用語をピックアップして解説します。
WebKit(ウェブキット)は、HTML や CSS などで記述されたドキュメントを解釈し、ユーザインタフェース上で視覚的に表現するための、ブラウザエンジン(レンダリングエンジン)と呼ばれるソフトウェアコンポーネントのひとつ。Apple が中心となって OSS として開発し、Safari や PlayStation 4 インターネットブラウザーなどで使用されています。
ITP は Safari の機能だと認識されることも多いようですが、実際は WebKit の機能です。また、iOS デバイス上で App Store を介し配信されるアプリが使用するブラウザエンジンは WebKit で、iOS 版の Google Chrome も WebKit を使用しています(iOS 版以外は Blink を使用)。
しかし、執筆次点では、ITP 機能を利用しているのは Safari のみのようです(一次情報が見つからないのですが)。
document.cookie を介して JavaScript で扱えるクッキーは、1st-party ドメインのクッキーです。
RFC 6265 の 5.3. Storage Model にあるように、Set-Cookie にて HttpOnly 属性が付けられたクッキーは、document.cookie で上書きすることはできません(ブラウザの種類やバージョンにより挙動が異なる)。
呼び名を意識することがあまり無いかもしれませんが、Set-Cookie や document.cookie に Expires 属性を付けないクッキーが Session Cookie で、付けたものが Persistent Cookie です。前者はブラウザを閉じると消えますが、後者はブラウザを閉じても指定した有効期限内は残ります。
effective top-level domain(.co.jp や .com など)に、ドメインラベルをもうひとつ追加した example.co.jp や example.com といったドメイン(top privately-controlled domain)を指します。www.example.co.jp や www.example.com は eTLD+1 ではありません(これらは eTLD+2)。
リンク先 URL のクエリ文字列などに追加のパラメータなどを仕込むことで、リンク元からリンク先に任意の情報を受け渡す仕組みです。
この仕組みを使うことで、リンク元からリンク先にページ訪問者の識別子を渡すことが可能になり、リンク先側でその識別子を 1st-party クッキーに保存しておけば、3rd-party クッキーを介さないクロスサイトトラッキングを実現できます。
リファラ URL が、リンクデコレーションとして利用されることもあります。
ITP では、次の条件を満たす場合に、リンクデコレーションによるクロスサイトトラッキングであると判定します。
HTTP リダイレクトを用いて行われるクロスサイトトラッキングです。この手法では、ユーザーを別のページにナビゲートする際、間にトラッカーサイトを挟みます。
例えば、サイト from-domain からサイト to-domain に移動する場合を考えます。通常は、from-domain から to-domain に直接リンクさせます。
from-domain --[Link]--> to-domain
1st-party バウンストラッキングでは、ユーザーが from-domain 上のリンクをクリックするとまず tracker-domain に移動し、そこから to-domain にリダイレクトさせます。この時、1st-party バウンストラッカーである tracker-domain は、ユーザーに関する情報を、自ドメインの 1st-party コンテキストでクッキーとして記録できます。
from-domain --[Link]--> tracker-domain --[Redirect]--> to-domain
3rd-party コンテキストでロードされたリソースに関する WebKit のキャッシュ機能です。
A partitioned cache means cache entries for third-party resources are double-keyed to their origin and the first-party eTLD+1. This prohibits cross-site trackers from using the cache to track users. Even so, our research has shown that trackers, in order to keep their practices alive under ITP, have resorted to partitioned cache abuse.
上の説明文は ITP のブログ記事からの引用です。パーティションキャッシュでは、3rd-party コンテキストでロードされたリソースのキャッシュを、そのリソースの Origin と 1st-party の eTLD+1 の組み合わせごとに隔離して保存します。
例えば、https://www.techscore.com 配下にある /3rd-page.html というリソースを、次の四つのページ配下にて 3rd-party コンテキストとして読み込んだとします。
(1) https://example.com/1st-page-1.html | +- https://www.techscore.com/3rd-page.html (2) https://example.com/1st-page-2.html | +- https://www.techscore.com/3rd-page.html (3) https://sub.example.com/1st-page-3.html | +- https://www.techscore.com/3rd-page.html (4) https://example.org/1st-page-4.html | +- https://www.techscore.com/3rd-page.html
結果として、https://www.techscore.com/3rd-page.html は、(1) (2) (3) で共通のキャッシュと、(4) のキャッシュの、二つが保存されます。(1) (2) (3) はいずれの 1st-party ドメインも eTLD+1 が example.com で、(4) のみ eTLD+1 が example.org であるからです(.com ではなく .org)。
このパーティションキャッシュは、クロスサイトトラッカードメインであるかどうかに関係なく、全ての 3rd-party コンテキスト内のリソースキャッシュに適用されます。
以降の解説においては、ユーザーが対象ドメインに対し、1st-party コンテキストで最後にアクセスした日時を UIT(User Interaction Timestamp)と呼ぶこととし、そこを起点としたタイムラインを記載しています。
ITP 2.0 の記事で次のように書かれており、(少なくとも ITP 2.0 以降は)ブラウザを使っていない期間を経過日数としてはカウントしないようです。
The user doesn’t use Safari which means that these three days do not count towards the 30 days before website data purge.
なお、ITP によって規制を受けるクッキーは、基本的にクロスサイトトラッカードメインの Persistent Cookie ですが、2019/12 に発表された Preventing Tracking Prevention Tracking からは、クロスサイトトラッカードメインだけにとどまらない規制がかかるようになりました。
以下、ところどころで ITP 特有の技術用語が出てきますが、それらは記事の後半に説明を掲載していますので、そちらをご参考ください。
【参考】
【参考】
【参考】
【参考】
【参考】
【参考】
【参考】
ITP では、アクセスしたドメイン(eTLD+1)がクロスサイトトラッキング機能を有するかどうかを、機械学習モデルを使って分類し、デバイス上に記録します。ここでクロスサイトトラッカーとして分類されたドメインが、ITP によって各種制限を受けることになります。
ITP 機能の一部として用意された特別なクッキー。クロスサイトトラッカーのクッキーを、パーティションキャッシュと同様に、1st-party の eTLD+1 ごとに隔離保存します。3rd-party コンテキストからのアクセスはできません。
3rd-party の埋め込み(iframe)が、自身が 1st-party コンテキストで作成したクッキーにアクセスする仕組み。iframe タグの sandbox 属性に allow-storage-access-by-user-activation を指定することで利用可能。
Storage Access API は、二つのメソッド document.hasStorageAccess(), document.requestStorageAccess() を持っています。
document.hasStorageAccess() は、ドキュメントがその 1st-party クッキーにアクセス可能であるかを boolean 値で示す Promise を返します。
document.requestStorageAccess() は、アクセスが許可された場合と拒否された場合で処理を切り分ける Promise を返します。
【参考】
みなさん、良いクリスマスを!
The post ITP 1.0 から 2.3+α までの解説 first appeared on TECHSCORE BLOG.
]]>The post Pythonで始めるアルゴレイヴ入門 first appeared on TECHSCORE BLOG.
]]>アルゴレイヴ(Algorave)というものをご存じでしょうか?アルゴレイヴとはAlgorithm(アルゴリズム)とRave(皆で楽しむこと)を組み合わせた造語です。音楽や映像をプログラミングで即興的に作り上げるライブコーディングという技術を使用し、ライブコーダーが様々なアルゴリズムから生成された音楽を流すイベントのことを指します。2011年イギリスで最初のイベントが開催され、現在世界で広がりつつあります。日本でも度々開催されています(algorave.tokyo)
ライブコーディングとは即興的にプログラムを書き/編集しながら、音楽や映像を奏でるリアルタイム・パフォーマンスのことです。作りこんだものではなく、偶然性や即興性を楽しむものでパフォーミングアートにおける新たな表現形態として注目されています。
とはいってもなかなか想像しづらいかもしれません。これから紹介するFoxDotの公式サイトでデモ動画が公開されているので一度見てみて下さい。きっと試してみたくなるはずです。
また、PyConJP2019でも紹介されていたのでこちらもご参照ください(Pythonでライブをしよう -FoxDotを使った新時代のPython活用法-)
詳しいインストール方法は公式ページをご覧ください
>>> FoxDot.start
$ python -m FoxDot
では早速音を鳴らしてみましょう。FoxDotのコンソールに
Clock.bpm = 60 p1 >> pluck()
と入力し、
CTRL+ENTERで実行します。すると弦をはじいたような音(プラック音)が鳴るはずです。これは1行目でテンポを指定し、二行目でメロディを奏でています。pluckの引数が指定されていないので4つ打ちで音が鳴っているはずです。また、pluckが音色を指定しています。bassやstar,sawなどに変更してみてください。音色が変わります。
音を止めるときには
p1.stop()で指定した音が停止し、
Ctrl+.ですべての音が停止します。
p1 >> pluck([0,2,4], dur=[1,1/2,1/2], amp=[1,3/4,3/4])
また複数の値を括弧で囲むことによって音をグループ化することもできます。
b1 >> bass([(0,9),(3,7)], dur=4, pan=(-1,1))
次のようにドラムも演奏可能です。
d1 >> play("x-o-")
d1 >> play("xxox") d2 >> play("---(-=)", pan=0.5)
d1 >>("x x x x ") d2 >>(" o o ") d3 >>("=-------")
d1 >> play("x[--]o(=[-o])")
>> print([1,2,3] *2)
>>> print([1,2,3] *2) [1, 2, 3, 1, 2, 3] >>> print(P[1,2,3] *2) P[2, 4, 6]
>>> print(P[1,2,3]*[2,3]) P[2, 6, 6, 3, 4, 9]
いかがだったでしょうか。今回の記事では導入からFoxDotでのコーディングの基礎まで説明しました。基礎ではありますが、これらを組み合わせることによって面白いパターンを生み出せます。是非皆さんもFoxDotをはじめAlgoraveを盛り上げていきましょう。Thanks
The post Pythonで始めるアルゴレイヴ入門 first appeared on TECHSCORE BLOG.
]]>はじめまして、内定者で大学生の松田と申します。
続きを読む...
The post 当社への就職活動とアルバイトを通じて感じたギャップ first appeared on TECHSCORE BLOG.
]]>はじめまして、内定者で大学生の松田と申します。
テックスコアを運営するシナジーマーケティングに、技術職として2020年入社予定で6月からアルバイトをさせていただいています。
私は以下の2点についてお話をさせていただきます。
少しでも社内の技術職についての様子が伝わればと思います。
1つ目に印象に残っていることは産休からの復職率の高さとフレックス制度などの社内制度があることで働きやすそうだということです。実際にアルバイトを始めてから社内を見てみますと7時に出社し、16時に帰宅する方や18時前に帰宅する方が多数見受けられることに驚きました。
2つ目に印象に残っていることは人事制度がしっかりしているということです。階級が細かく設定されており評価者も複数いることで公正な判断を行う制度があると感じました。
1次面接から最終面接までの流れです。(※あくまでも私の流れであって一人ひとり異なります)
※採用詳細はこちら
希望の部署の方にお越しいただいて働き方や仕事内容、所属部署の人数や役割など実際に仕事をしていて感じることや今まで聞けなかった会社で使用している技術的なことを1時間ほど質問する時間をいただきました。
印象に残っていることは面接の回数が人によって変わること、エントリーシートを提出すると1次面接に進むこと、異なる面接官に同じ質問をしても返答が異なることです。面接に関してとてもしっかりと準備をしていただいており、雰囲気や面接官の印象だけで判断するのではなく複数人の意見や会社としての判断基準でしっかりと面接の合否判定をしていただいたと当時でも現在でも感じております。
アルバイトを始めた6~8月の課題であるVirtualBox、Gitの操作、データベースについては既に使用したことがあったのでスムーズに進むものだと考えていましたがシナジーマーケティングの一員として行うことは1人で開発を行うことよりも正確性や基準を満たした内容が必要なので、必要な時間も完成物も大きく異なるものでした。
1人で開発環境を用意し、開発を行う場合は何を使用してもよく、完成物の確認をするのもほとんど自身で行っていたので動いたら終わりでしたが、会社として作成したものはどのようなことをしたのか記録していき出来上がったものも自分以外が保守をするのでわかりやすいコードを書く必要があります。何故その書き方をしたのか説明でき、書き直すにしてもどの部分に影響するのかを意識しながらコードを書いたり開発環境を整えたりすることを教わりました。
9月から12月では主要製品のSynergy!について色々な課題や実務などを通して学ばせていただいております。Synergy!やwebに必要な言語を教えていただく過程でマーケティングについても学ばせていただきました。
始めに私の考えと異なっていたことはアルバイトの時にサポートしてくれる方は1人でテストやインプットすることがほとんどだと思っていたのですがグループ(3人)でサポートしてもらい質問がある時や困った時に助けていただける体制があり、課題についてはアウトプットする機会を多くいただける学びやすい環境を用意してもらえたことです。
社員の方々については新卒採用が多く勤続年数は長い方が多いと思っていましたが、中途採用が多数で私が接した方々の勤続年数は5年以内が多いことです。
社内では部署をまたいで適度にコミュニケーションがあり、席に座ってチャットや電話だけを使用するのではなく、用事がある人の席までいって要件を話している姿も見受けられます。社員の方々は趣味をもっている方が多く部署をまたいで社外で同じ活動をされている方が一定数いることに驚きました。
この記事が少しでもこれからシナジーマーケティングへエントリーする方の一助になれば幸いです。
The post 当社への就職活動とアルバイトを通じて感じたギャップ first appeared on TECHSCORE BLOG.
]]>これは 😺TECHSCORE Advent Calendar 2019😺 の 20 日目の記事です。
こんにちは。桂川です。社会人として働き始めて約 9 カ月が経過しました。
続きを読む...
The post あえて言うほどではない 数値 ⇔ 文字列変換 2019 年人気プログラミング言語トップ 10 編 first appeared on TECHSCORE BLOG.
]]>これは TECHSCORE Advent Calendar 2019 の 20 日目の記事です。
こんにちは。桂川です。社会人として働き始めて約 9 カ月が経過しました。これまでに複数のプログラミング言語に触れる機会がありました。未経験のプログラミング言語に触れるとしても、基本的な記述方法は勘でわかることが多いです。しかし、やはりプログラミング言語ごとに相違点があるため、 Web などを駆使して調べつつコーディングをしています。
本ブログでは 2012 年 11 月 28 日に公開された「あえて言うほどではない 数値 ⇔ 文字列変換 Java編」という記事があります。この記事では、コーディングをしているときに頻繁に記述するであろう“文字列 ⇔ 数値”の変換について、Java における記述方法を紹介しています。個人的にも Java で“文字列 ⇔ 数値変換”を実装するとき、何度も見てしまう、お世話になっているページです。(ぜひ、みなさまもご覧ください!)
複数のプログラミング言語に触れる機会があった自分としては、Java だけではなく他のプログラミング言語ではどのように記述できるのか、どのような相違点があるのかを把握したいと思いました。このようなモチベーションで、本記事では複数のプログラミング言語における記述について紹介します。この記事が、さまざまなプログラミング言語に触れる方々にとってのコードスニペットのような役目を果たすことができたなら幸いです。
本記事で対象とするプログラミング言語は、ソフトウェア開発のプラットフォームである GitHub が発表した年次レポート Octoverse より、“2019 年人気プログラミング言語トップ 10”( Octoverse における Top languages )とします[1]。
以下、各プログラミング言語における数値 ⇔ 文字列変換を実現するコードを紹介します。
※今回、扱う数値は整数型に限定します。また、変換方法については抜粋しており、すべてを網羅しているわけではございません。以上について、あらかじめご了承くださいませ。
JavaScript はインタラクティブな Web サイトの構築、ゲームの制作にも使用される主要な言語です。変数の型をコンパイルや実行より前に決めないという、動的型付けの言語です。JavaScript における数値 ⇔ 文字列変換は以下のように記述できます。JavaScript の実行環境である Node.js のバージョンは 12.13.1 を使用しました。
$ node -v 12.13.1
String
オブジェクトを使用して数値を文字列に変換する例。
var i = 2019; var s = String(i);
Number
クラスの toString
メソッドを使用して数値を文字列に変換する例。
var i = 2019; var s = i.toString();
空白の文字列を足して数値を文字列に変換する例。
var i = 2019; var s = i + '';
${}
を使用して数値を文字列に変換する例。
var i = 2019; var s = `${i}`;
Number
オブジェクトを使用して文字列を数値に変換する例。
var s = '2019'; var i = Number(s);
parseInt
オブジェクトを使用して文字列を数値に変換する例。
var s = '2019'; var i = parseInt(s); // もしくは var i = parseInt(s, 10);
四則演算によって文字列を数値に変換する例。
var s = '2019'; var i = s - 0; // もしくは var i = s * 1; var i = s / 1;
Python はさまざまな用途で使用されます。特に近年では人工知能を使用したアプリケーションの開発やデータサイエンスの分野で使用される言語として注目されています。動的型付けの言語です。また、プログラミング初学者にとって学習しやすい言語としても認知されています。Python における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 3.6.8 です。
$ python --version 3.6.8 (default, Oct 7 2019, 12:59:55) [GCC 8.3.0]
組み込み関数 str
を使用して数値を文字列に変換する例。
i = 2019 s = str(i)
組み込み関数 int
を使用して文字列を数値に変換する例。
s = '2019' i = int(s)
そして、unicodedata
モジュールで定義されている関数 unicodedata.numeric()
を使用することにより、一、十、百、千といった漢数字 1 つの数値への変換が可能です。これはシブいですね。
import unicodedata s = '百' i = unicodedata.numeric(s)
Java はオブジェクト指向に基づいた言語です。モバイルおよび Web アプリケーション、ゲーム、データベース駆動型ソフトウェアの開発などに使用されます。変数の型をコンパイルや実行より前にあらかじめ決めるという、静的型付けの言語です。Java における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 1.8.0_202 です。
$ java -version java version "1.8.0_202" Java(TM) SE Runtime Environment (build 1.8.0_202-b08) Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
String
クラスの valueOf
メソッドを使用して数値を文字列に変換する例。
int i = 2019; String s = String.valueOf(i);
空の文字列に足すことにより数値を文字列に変換する例。
int i = 2019; String s = "" + i;
Integer
クラスのインスタンスを生成して、toString()
メソッドを使用して数値を文字列に変換する例。
int i = 2019; String s = new Integer(i).toString();
Integer
クラスの toString
メソッドを使用して数値を文字列に変換する例。
int i = 2019; String s = Integer.toString(i);
Integer
クラスの parseInt
メソッドを使用して文字列を数値に変換する例。
String s = '2019'; int i = Integer.parseInt(s);
Integer
クラスのインスタンスを生成して、intValue()
メソッドを使用して文字列を数値に変換する例。
String s = '2019'; int i = new Integer(s).intValue();
Integer
クラスの valueOf
メソッドを使用して文字列を数値に変換する例。
String s = '2019'; int i = Integer.valueOf(s);
PHP は Web 開発に適しており、HTML に埋め込みが可能な言語です。動的型付けの言語です。PHP における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 7.2.26 です。
$ php -v PHP 7.2.26 (cli) (built: Dec 17 2019 14:06:22) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
strval
関数を使用して数値を文字列に変換する例。
$i = 2019; $s = strval($i);
sprintf
関数を使用して数値を文字列に変換する例。
$i = 2019; $s = sprintf("%d",$i);
キャスト演算子を使用して数値を文字列に変換する例。
$i = 2019; $s = (string) $i;
intval
関数を使用して文字列を数値に変換する例。
$str = "2019"; $num10 = intval($str);
キャスト演算子を使用して文字列を数値に変換する例。
$str = "2019"; $num = (int)$str;
C# は、C から派生した言語です。 Java と似た言語です。静的型付けの言語です。モバイルアプリ、ゲーム、エンタープライズソフトウェアの開発などに使用されます。C# における数値 ⇔ 文字列変換は以下のように記述できます。使用したコンパイラは mcs で、バージョンは 6.6.0.161 です。
$ mcs --version Mono C# compiler version 6.6.0.161
ToString
メソッドを使用して数値を文字列に変換する例。
int i = 2019; string s = i.ToString();
Convert
クラスの ToString
メソッドを使用して数値を文字列に変換する例。Convert
クラスで変換する場合には、null
の変換を試みても例外が発生しません。
int i = 2019; string s = Convert.ToString(i);
int
クラスの Parse
メソッドを使用して文字列を数値に変換する例。
string s = "2019"; int i = int.Parse(s);
Convert
クラスを使用して文字列を数値に変換する例。
string s = "2019"; int i = Convert.ToInt32(s);
C++ は C から派生した言語です。オペレーティングシステム、ブラウザ、ゲームなどにおける中核的な言語として、広く使用されています。静的型付けの言語です。C++ における数値 ⇔ 文字列変換は以下のように記述できます。対象としたバージョンは C++11 で、使用したコンパイラは g++ でバージョンが 4.8.5 です。
$ g++ --version g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39) Copyright (C) 2015 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
to_string
関数を使用して数値を文字列に変換する例。
int i = 2019 std::string s = std::to_string(i);
stoi
関数を使用して文字列を数値に変換する例。
std::string s = "2019"; int i = std::stoi(s);
TypeScript は、JavaScript において課題として考えられている点を補う目的で開発された言語です。JavaScript が動的型付けの言語であるのに対して、静的型付けの言語であるという特徴が挙げられます。これにより、意図していない型の値を変数に代入できないため、実行前に不具合の検出が可能であるというメリットがあります。TypeScript における数値 ⇔ 文字列変換は JavaScript の記述の使用が可能であるということで割愛させていただきます。
Shell はオペレーティングシステムの操作などに使用できる言語です。Bashなどで拡張された場合はともかく、Shell の変数にはデータ型が定義されておらず、一律で文字列型です。それゆえ、数値 ⇒ 文字列という処理は存在しません。ただし、四則演算など数値の計算が可能です。使用した Shell は Bash でバージョンは 4.4.20 です。
$ bash --version GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu) Copyright (C) 2011 Free Software Foundation, Inc. ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html> This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
計算を実施する場合は対象の部分を $()
で囲み、数値として展開する変数の直前に expr
を使用します。この時、四則演算記号の前後にスペースが必要です。※掛け算や括弧を使用する場合は \*
のように、記号の前にバックスラッシュ(Windows では円マーク)を記述してエスケープをする必要があります。
s=2019 s=$(expr $s + 1) # 文字列sは計算時のみ一時的に数値として扱われる
C は、今回の対象としている Java、PHP、 JavaScript、C#、C++ といった多くの言語のルーツとされている言語です。静的型付けの言語です。C における数値 ⇔ 文字列変換は以下のように記述できます。対象としたバージョンは C11 で、使用したコンパイラは gccでバージョンが 4.8.5 です。
$ gcc --version gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39) Copyright (C) 2015 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
sprintf
関数を使用して数値を文字列に変換する例。
int i = 2019; char s[256]; sprintf(s,"%d",i);
atoi
関数を使用して文字列を数値に変換する例。
char* s = "2019"; int i = atoi(s);
他にも atol
、atoll
、atof
という関数もあり、これらを使用することで long 型、long long 型、float 型への変換が可能です。
Ruby はまつもとゆきひろ氏により開発されたオブジェクト指向のスクリプト言語です。特徴として、シンプルさが挙げられます。動的型付けの言語です。Ruby における数値 ⇔ 文字列変換は以下のように記述できます。使用したバージョンは 2.0.0 です。
$ ruby --version ruby 2.0.0p648 (2015-12-16) [x86_64-linux]
to_s
メソッドを使用して数値を文字列に変換する例。
i = 2019 s = i.to_s
to_i
メソッドを使用して文字列を数値に変換する例。
s = "2019" i = s.to_i
可読性が高いですね。
“2019 年人気プログラミング言語トップ 10”を対象に、数値 ⇔ 文字列変換を実現するコードを紹介しました。類似している記述方法もあれば、そうでない記述方法もありましたね。これでどんなプログラミング言語を使うとしても、“数値 ⇔ 文字列変換”はバッチリです…
The post あえて言うほどではない 数値 ⇔ 文字列変換 2019 年人気プログラミング言語トップ 10 編 first appeared on TECHSCORE BLOG.
]]>今年の8月にRuby on Rails(以下Rails)の最新バージョンとなる 6.0 がリリースされました。
続きを読む...
The post Ruby on Rails 6.0のAction Textを触ってみた。 first appeared on TECHSCORE BLOG.
]]>今年の8月にRuby on Rails(以下Rails)の最新バージョンとなる 6.0 がリリースされました。
RailsはTECHSCOREブログで何度か取り上げているフレームワークです。
いくつか新機能が追加されていますが、
その中で Action Text という機能が面白そうだったため触ってみました。
【実行環境】 OS :CentOS 7.6 … Windows10上にOracle VM VirtualBoxを導入して実行 Ruby :2.6.3 Rails :6.0.1
Action Text はリッチテキストを簡単に作成、編集、表示することができるフレームワークを提供します。
(公式ブログの内容を意訳)
https://weblog.rubyonrails.org/2018/10/3/introducing-action-text-for-rails-6/
すなわち「HTMLやCSSの知識がなくても文章の修飾や画像の挿入などができるエディタ」
が提供されるということになります。
リッチテキストエディタ自体は、もともとgem(Rubyのライブラリ)で提供されていたようですが、
今回のバージョンアップで本体に組み込まれるようになりました。
それでは、実際にAction Text を使ってみます。
Railsのインストール方法などは、公式ガイドにお任せしますので、
rails new(Railsアプリのひな型を作るコマンド)で生成されたアプリをもとに
プログラムを実装していきます。
# CRUD機能追加(playground はモデル名です) rails g scaffold playground # action_textをインストール rails action_text:install
画像を扱うために ImageMagick をOS側にインストールします。
参考:https://imagemagick.org/script/download.php
# libをインストール yum install https://imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-libs-7.0.9-8.x86_64.rpm # 本体をインストール yum install https://imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-7.0.9-8.x86_64.rpm
次にRailsでImageMagickを利用するための設定を行います。
Gemfile
# 以下の行がコメントアウトされているので外して保存する。 gem 'image_processing', '~> 1.2'
上記保存後、bundle installをします。
bundle install
データベースのマイグレーションをします。
rake db:migrate
基本的な設定はできたので、モデルやコントローラ、ビューに設定を書き加えていきます。
app/models/playground.rb
class Playground < ApplicationRecord has_rich_text :content end
app/controllers/playgrounds_controller.rb
def playground_params params.require(:playground).permit(:content) end
app/views/playgrounds/_form.html.erb
# 下記のコードがリッチテキストエリアになりますので、適切な場所に追加します。 <div class="field"> <%= form.label :content %> <%= form.rich_text_area :content %> </div>
app/views/playgrounds/show.html.erb
# 下記のコードがcontent(=リッチテキスト)を表示する箇所になります。 <%= @playground.content %>
ここまで実施すれば基本的な設定は完了です。
rails s などのコマンドでサーバを立ち上げて確かめてみましょう。
このような感じで、複雑な設定を行わずにリッチテキストを扱うことができました。
Action Text で提供されるリッチテキストエディタは Trix と呼ばれるものが利用されています。
公式によると見た目をカスタマイズできるらしいので試してみます。
action_textをインストールした際 app/assets/stylesheets フォルダ以下に
actiontext.scss ファイルが生成されていますので、こちらを編集していきます。
trixのCSSをもとに作成しています。
app/assets/stylesheets/actiontext.scss
trix-editor { border: 5px outset #bbb; } trix-toolbar .trix-button--icon-bold::before { background-image: url(bold.png); } trix-toolbar .trix-button--icon-italic::before { background-image: url(italic.png); } trix-toolbar .trix-button--icon-strike::before { background-image: url(strike.png); } trix-toolbar .trix-button--icon-link::before { background-image: url(link.png); }
※ボタン画像はapp/assets/images 以下に保存しています。
試してみた結果がこちらです。
今回は入力欄の枠とボタンの画像を変更してみました。
今回はRails 6.0 の新機能である Action Text の基本的なところについて触れました。
公式から提供されているだけあって簡単に導入でき、様々な応用が利きそうな機能だと感じました。
ただし今回触れたものはあくまでも基本だけですので、
実務でAction Text を使うためには、さらに理解を深める必要がありそうです。
The post Ruby on Rails 6.0のAction Textを触ってみた。 first appeared on TECHSCORE BLOG.
]]>今までCloudFormationテンプレートを直接YAMLで書いていましたが、作成するリソース数が多くなるにつれて記述量の多さに辛さを感じるようになってきました。
続きを読む...
The post AWS CDKを導入して脱YAMLテンプレートを試みる first appeared on TECHSCORE BLOG.
]]>今までCloudFormationテンプレートを直接YAMLで書いていましたが、作成するリソース数が多くなるにつれて記述量の多さに辛さを感じるようになってきました。
そんな折AWS CDKのことを知り、試しに簡単な構成をAWS CDKで作成してみることにしました。
Developer GuideやAPI Referenceを見ると、AWS CDKのAPIにはCloudFormationの各リソースタイプ(VPC等)と1対1で対応している低レベルなものと、より高レベルなもの(例えば、VPCやサブネットを作成する際にインターネットゲートウェイやルートテーブル、ルートといった関連リソースを自動的に作成・関連付けしてくれる)があるようですが、利用するAZやサブネットの個数を柔軟にコントロールしたかったので、今回は低レベルAPIの方を試してみたいと思います。
Getting Started With the AWS CDKに従って、AWS CDKを実行する環境を準備します。
AWS CDKの言語には、Pythonを選択します。
curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash - sudo yum install python3 sudo yum install nodejs sudo npm install -g aws-cdk
今回確認に利用した環境は、以下の通りです。
OS: Amazon Linux release 2 (Karoo)
node: v10.17.0
npm: 6.11.3
Python: 3.7.4
pip: 19.0.3
cdk: 1.18.0
AWSのクレデンシャルを環境変数に設定しておきます。
export AWS_ACCESS_KEY_ID=Specifies your access key. export AWS_SECRET_ACCESS_KEY=Specifies your secret access key. export AWS_DEFAULT_REGION=ap-northeast-1
IAMのアクセス権限には、以下のAWS管理ポリシーをアタッチしておきます。
AWSCloudFormationFullAccess AmazonEC2FullAccess AmazonS3FullAccess
パブリックサブネット(/20)とプライベートサブネット(/20)をAZごとに1つずつ持つVPC(10.0.0.0/16)を作成します。
パブリックサブネットにはNAT Gatewayを配置します。
Getting Started With the AWS CDKにあるようにアプリの雛形を作成し、AWS CDKのモジュールをインストールします。
今回はVPCとサブネットを作成しますので、コアモジュールとEC2モジュールをインストールします。
その他のモジュールについてはAWS CDK Python Referenceを参照してください。
mkdir my-network cd my-network cdk init --language python source .env/bin/activate pip install -r requirements.txt pip install --upgrade aws-cdk.core pip install --upgrade aws-cdk.aws_ec2
雛形を作成した時点では、以下のような構成となっています。
└── my-network ├── README.md ├── app.py ├── cdk.json ├── my_network │ ├── __init__.py │ ├── my_network.egg-info │ │ ├── PKG-INFO │ │ ├── SOURCES.txt │ │ ├── dependency_links.txt │ │ ├── requires.txt │ │ └── top_level.txt │ └── my_network_stack.py ├── requirements.txt └── setup.py
my_network_stack.py
を開くと、以下のようなコードが記述されています。
from aws_cdk import core class MyNetworkStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # The code that defines your stack goes here
コメントにあるように、この中に作成するリソースのコードを記述していきます。
作成するリソースの数が多いので、my_resourcesディレクトリを作成してそこに追加したモジュールを呼び出すようにします。
モジュール追加後の構成は以下の通りです。
└── my-network ├── README.md ├── app.py ├── cdk.json ├── my_network │ ├── __init__.py │ ├── my_network.egg-info │ │ ├── PKG-INFO │ │ ├── SOURCES.txt │ │ ├── dependency_links.txt │ │ ├── requires.txt │ │ └── top_level.txt │ └── my_network_stack.py │ └── my_resources │ ├── __init__.py │ ├── availability_zone.py │ ├── nat_gateway.py │ ├── route.py │ ├── subnet.py │ └── vpc.py ├── requirements.txt └── setup.py
追加したモジュールの内容は以下の通りです。
availability_zone.py(アベイラビリティーゾーンの定義)※東京リージョンに限定
class AvailabilityZone: def __init__(self, region='ap-northeast-1') -> None: self.__region = region if self.__region == 'ap-northeast-1': self.__names = ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d'] else: self.__names = [] @property def names(self) -> list: return self.__names def name(self, az_number) -> str: return self.__names[az_number]
nat_gateway.py(NAT Gateway)
import hashlib from aws_cdk import ( core, aws_ec2, ) def create_nat_gateway(scope: core.Construct, vpc: aws_ec2.CfnVPC, subnet: aws_ec2.CfnSubnet) -> aws_ec2.CfnNatGateway: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() subnet_id = [tag['value'] for tag in subnet.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(subnet_id.encode()).hexdigest() eip = aws_ec2.CfnEIP(scope, f'{vpc_id}/EIP-{id}') nat_gateway = aws_ec2.CfnNatGateway(scope, f'{vpc_id}/NatGateway-{id}', allocation_id=eip.attr_allocation_id, subnet_id=subnet.ref, tags=[core.CfnTag( key='Name', value=f'{vpc_id}/NatGateway-{id}', )], ) return nat_gateway
route.py(ルートテーブル、ルート)
import hashlib from aws_cdk import ( core, aws_ec2, ) def create_privagte_route_table(scope: core.Construct, vpc: aws_ec2.CfnVPC, nat_gateway: aws_ec2.CfnNatGateway) -> aws_ec2.CfnRouteTable: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() ngw_id = [tag['value'] for tag in nat_gateway.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(ngw_id.encode()).hexdigest() route_table = aws_ec2.CfnRouteTable(scope, f'{vpc_id}/RouteTable-{id}', vpc_id=vpc.ref, tags=[core.CfnTag( key='Name', value=f'{vpc_id}/RouteTable-{id}', )], ) aws_ec2.CfnRoute(scope, f'{vpc_id}/Route-{id}', route_table_id=route_table.ref, destination_cidr_block='0.0.0.0/0', nat_gateway_id=nat_gateway.ref, ) return route_table def create_public_route_table(scope: core.Construct, vpc: aws_ec2.CfnVPC, internet_gateway: aws_ec2.CfnInternetGateway) -> aws_ec2.CfnRouteTable: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() igw_id = [tag['value'] for tag in internet_gateway.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(igw_id.encode()).hexdigest() route_table = aws_ec2.CfnRouteTable(scope, f'{vpc_id}/RouteTable-{id}', vpc_id=vpc.ref, tags=[core.CfnTag( key='Name', value=f'{vpc_id}/RouteTable-{id}', )], ) aws_ec2.CfnRoute(scope, f'{id}/Route-{id}', route_table_id=route_table.ref, destination_cidr_block='0.0.0.0/0', gateway_id=internet_gateway.ref, ) return route_table def create_route_table_association(scope: core.Construct, vpc: aws_ec2.CfnVPC, subnet: aws_ec2.CfnSubnet, route_table: aws_ec2.CfnRouteTable) -> aws_ec2.CfnSubnetRouteTableAssociation: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() subnet_id = [tag['value'] for tag in subnet.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(subnet_id.encode()).hexdigest() association = aws_ec2.CfnSubnetRouteTableAssociation(scope, f'{vpc_id}/SubnetRouteTableAssociation-{id}', route_table_id=route_table.ref, subnet_id=subnet.ref, ) return association
subnet.py(サブネット)
import hashlib import ipaddress import uuid from aws_cdk import ( core, aws_ec2, ) from my_resources import ( availability_zone, vpc, ) class SubnetGroup: def __init__(self, scope: core.Construct, vpc: aws_ec2.CfnVPC, *, desired_layers: int=2, desired_azs: int=2, region: str='ap-northeast-1', private_enabled: bool=True, cidr_mask: int=20) -> None: self._cidr_mask = cidr_mask self._desired_azs = desired_azs self._desired_layers = desired_layers self._private_enabled = private_enabled self._region = region self._reserved_azs = 5 self._reserved_layers = 3 self._scope = scope self._vpc = vpc self._desired_subnet_points = [] for layer_number in range(self._desired_layers): for az_number in range(self._desired_azs): self._desired_subnet_points.append([layer_number, az_number]) self._public_subnets = [] self._private_subnets = [] @property def cidr_mask(self) -> int: return self._cidr_mask @property def desired_azs(self) -> int: return self._desired_azs @property def desired_layers(self) -> int: return self._desired_layers @property def desired_subnet_points(self) -> list: return self._desired_subnet_points @property def private_enabled(self) -> bool: return self._private_enabled @property def private_subnets(self) -> list: return self._private_subnets @property def public_subnets(self) -> list: return self._public_subnets @property def region(self) -> str: return self._region @property def reserved_azs(self) -> int: return self._reserved_azs @property def reserved_layers(self) -> int: return self._reserved_layers @property def scope(self) -> core.Construct: return self._scope @property def vpc(self) -> aws_ec2.CfnVPC: return self._vpc def create_subnets(self) -> None: nw = ipaddress.ip_network(self.vpc.cidr_block) cidrs = list(nw.subnets(new_prefix=self.cidr_mask)) cidrs.reverse() az = availability_zone.AvailabilityZone() vpc_id = [tag['value'] for tag in self.vpc.tags.render_tags() if tag['key'] == 'Name'].pop() for layer in range(self.reserved_layers): for az_number in range(self.reserved_azs): current = [layer, az_number] cidr = str(cidrs.pop()) if current in self.desired_subnet_points: id = hashlib.md5(f'{layer}-{az_number}'.encode()).hexdigest() subnet = aws_ec2.CfnSubnet(self.scope, f'{vpc_id}/Subnet-{id}', cidr_block=cidr, vpc_id=self.vpc.ref, availability_zone=az.name(az_number), tags=[ core.CfnTag( key='Name', value=f'{vpc_id}/Subnet-{id}', ), core.CfnTag( key='Layer', value=f'{layer}', ), core.CfnTag( key='AZNumber', value=f'{az_number}', ), ], ) if self.private_enabled and layer > 0: self._private_subnets.append(subnet) else: self._public_subnets.append(subnet)
vpc.py(VPC、インターネットゲートウェイ)
from aws_cdk import ( core, aws_ec2, ) def create_vpc(scope: core.Construct, id: str, *, cidr='10.0.0.0/16', enable_dns_hostnames=True, enable_dns_support=True) -> aws_ec2.CfnVPC: vpc = aws_ec2.CfnVPC(scope, id, cidr_block=cidr, enable_dns_hostnames=enable_dns_hostnames, enable_dns_support=enable_dns_support, tags=[core.CfnTag( key='Name', value=id, )] ) return vpc def create_internet_gateway(scope: core.Construct, vpc: aws_ec2.CfnVPC) -> aws_ec2.CfnInternetGateway: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() internet_gateway = aws_ec2.CfnInternetGateway(scope, f'{vpc_id}/InternetGateway', tags=[core.CfnTag( key='Name', value=f'{vpc_id}/InternetGateway', )] ) aws_ec2.CfnVPCGatewayAttachment(scope, f'{vpc_id}/VPCGatewayAttachment', vpc_id=vpc.ref, internet_gateway_id=internet_gateway.ref, ) return internet_gateway
network_stack.py
を以下のように編集して、追加したモジュールを呼び出します。
from aws_cdk import core from my_resources import ( vpc, subnet, nat_gateway, route, ) class MyNetworkStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # The code that defines your stack goes here vpc_id = 'MyVPC' subnet_desired_layers = 2 subnet_desired_azs = 2 private_subnet_enabled=True v = vpc.create_vpc(self, vpc_id) igw = vpc.create_internet_gateway(self, v) subnet_group = subnet.SubnetGroup(self, v, desired_layers=subnet_desired_layers, desired_azs=subnet_desired_azs, private_enabled=private_subnet_enabled) subnet_group.create_subnets() if subnet_group.public_subnets: public_route_table = route.create_public_route_table(self, v, igw) for public_subnet in subnet_group.public_subnets: route.create_route_table_association(self, v, public_subnet, public_route_table) if subnet_group.private_subnets: private_route_tables = [] for public_subnet in subnet_group.public_subnets: ngw = nat_gateway.create_nat_gateway(self, v, public_subnet) private_route_table = route.create_privagte_route_table(self, v, ngw) private_route_tables.append(private_route_table) for private_subnet in subnet_group.private_subnets: az_number = [tag['value'] for tag in private_subnet.tags.render_tags() if tag['key'] == 'AZNumber'].pop() route.create_route_table_association(self, v, private_subnet, private_route_tables[int(az_number)])
準備ができましたので、アプリのルートディレクトリ(my-network)でcdkを実行してみます。
まずは、
cdk diff
を実行して作成・変更されるリソースの差分を確認してみます。
cdk diff
Stack my-network Conditions [+] Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]} Resources [+] AWS::EC2::VPC MyVPC MyVPC [+] AWS::EC2::InternetGateway MyVPC--InternetGateway MyVPCInternetGateway [+] AWS::EC2::VPCGatewayAttachment MyVPC--VPCGatewayAttachment MyVPCVPCGatewayAttachment [+] AWS::EC2::Subnet MyVPC--Subnet-c7763203e20a64b270352752d6a1e7c6 MyVPCSubnetc7763203e20a64b270352752d6a1e7c6 [+] AWS::EC2::Subnet MyVPC--Subnet-c2eb282156233b5d827219971c8b04c2 MyVPCSubnetc2eb282156233b5d827219971c8b04c2 [+] AWS::EC2::Subnet MyVPC--Subnet-eca26941bc5187d1e2983961edb6dbb6 MyVPCSubneteca26941bc5187d1e2983961edb6dbb6 [+] AWS::EC2::Subnet MyVPC--Subnet-ea66c06c1e1c05fa9f1aa39d98dc5bc1 MyVPCSubnetea66c06c1e1c05fa9f1aa39d98dc5bc1 [+] AWS::EC2::RouteTable MyVPC--RouteTable-e7636240538bdd71bd55872aed605e26 MyVPCRouteTablee7636240538bdd71bd55872aed605e26 [+] AWS::EC2::Route e7636240538bdd71bd55872aed605e26--Route-e7636240538bdd71bd55872aed605e26 e7636240538bdd71bd55872aed605e26Routee7636240538bdd71bd55872aed605e26 [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCSubnetRouteTableAssociation8a80275c4aeaac9a8e6f6f36e18f5f5b [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-7c555b7a2a9e217d5de327ca36a79a54 MyVPCSubnetRouteTableAssociation7c555b7a2a9e217d5de327ca36a79a54 [+] AWS::EC2::EIP MyVPC--EIP-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCEIP8a80275c4aeaac9a8e6f6f36e18f5f5b [+] AWS::EC2::NatGateway MyVPC--NatGateway-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCNatGateway8a80275c4aeaac9a8e6f6f36e18f5f5b [+] AWS::EC2::RouteTable MyVPC--RouteTable-f178a135091a35c9541fbf72fb1c8dc1 MyVPCRouteTablef178a135091a35c9541fbf72fb1c8dc1 [+] AWS::EC2::Route MyVPC--Route-f178a135091a35c9541fbf72fb1c8dc1 MyVPCRoutef178a135091a35c9541fbf72fb1c8dc1 [+] AWS::EC2::EIP MyVPC--EIP-7c555b7a2a9e217d5de327ca36a79a54 MyVPCEIP7c555b7a2a9e217d5de327ca36a79a54 [+] AWS::EC2::NatGateway MyVPC--NatGateway-7c555b7a2a9e217d5de327ca36a79a54 MyVPCNatGateway7c555b7a2a9e217d5de327ca36a79a54 [+] AWS::EC2::RouteTable MyVPC--RouteTable-765dc824da4c4f28ce886a61c6b54742 MyVPCRouteTable765dc824da4c4f28ce886a61c6b54742 [+] AWS::EC2::Route MyVPC--Route-765dc824da4c4f28ce886a61c6b54742 MyVPCRoute765dc824da4c4f28ce886a61c6b54742 [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-ad32a7208c1d822e4f35b34060485714 MyVPCSubnetRouteTableAssociationad32a7208c1d822e4f35b34060485714 [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-c73221f09ade1614a962f2c9c60cd682 MyVPCSubnetRouteTableAssociationc73221f09ade1614a962f2c9c60cd682
定義したリソースが追加の対象となっていることが確認できましたので、
cdk deploy
を実行して実際にリソースを作成します。
cdk deploy
my-network: deploying... my-network: creating CloudFormation changeset... ... my-network
cdk deploy
の実行完了後、AWSマネジメントコンソールでCloudFormationスタックを見ると、my-networkスタックでリソースが作成されていることが確認できました。
今回はAWS CDKの低レベルAPIを試してみましたが、プログラミング言語のループ構造を利用して同じリソースを複数作成できることだけでも、YAMLで直接記述するのと比べてコード量を大幅に減らすことができ、非常に魅力的なものだと感じました。
CloudFormationでリソースを作成したことがあればAPIは直感的に利用できるものとなっており、導入までの敷居は比較的低いのではないかと思います。
今後は本番環境での利用も想定して使っていきたいと思います。
The post AWS CDKを導入して脱YAMLテンプレートを試みる first appeared on TECHSCORE BLOG.
]]>The post OpenAPI Generator で OAuth2 アクセストークン発行のコードまで生成してみる first appeared on TECHSCORE BLOG.
]]>OpenAPI Generator は OpenAPI Specification の定義ファイルがあれば、API クライアントやサーバのスタブのコードを自動生成してくれるという便利な代物です。
ただ、生成されたコードがそのまま使えるとは言えません。例えば、OAuth2 で保護されている API にアクセスする場合、アクセストークンの発行が必要ですが、OpenAPI Generator のクライアントコード生成ではアクセストークン発行のコードは生成してくれません。
Java のクライアントコードを生成した場合、README に以下のようなサンプルコードが出力されますが、 setAccessToken してね、ということくらいしか書かれておらず、アクセストークンの発行の部分には触れられていません。
(Java のクライアントコード生成では、デフォルトで OkHttp 3 を利用したコードが生成されます。OkHttp 3 のクライアントコードの場合、一応アクセストークン発行のコードは生成されますが、Apache Oltu という既にプロジェクト終了したライブラリを使ったコードなので、あまり実案件では使いたくありません)
public static void main(String[] args) { ApiClient defaultClient = Configuration.getDefaultApiClient(); defaultClient.setBasePath("https://mail.paas.crmstyle.com/e"); // Configure OAuth2 access token for authorization: mail_oauth OAuth mail_oauth = (OAuth) defaultClient.getAuthentication("mail_oauth"); mail_oauth.setAccessToken("YOUR ACCESS TOKEN"); DefaultApi apiInstance = new DefaultApi(defaultClient); String user = "user_example"; // String | user name DeliverySettingRequest deliverySettingRequest = new DeliverySettingRequest(); // DeliverySettingRequest | try { DeliveryResponse result = apiInstance.createMailDeliverySetting(user, deliverySettingRequest); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling DefaultApi#createMailDeliverySetting"); System.err.println("Status code: " + e.getCode()); System.err.println("Reason: " + e.getResponseBody()); System.err.println("Response headers: " + e.getResponseHeaders()); e.printStackTrace(); } }
また、Spring WebFlux をライブラリとしてクライアントコード生成も可能ですが、これに関してはアクセストークン発行はおろか、そのままビルドも通らないという状況です。
今回は、 OpenAPI Generator で Spring WebFlux のクライアントコードを OAuth2 のアクセストークン発行付きで生成するところまでを試してみました。
実行環境は以下の通りです。OpenAPI Generator のバージョンは、現時点の最新安定版の 4.2.2 を使いました。
$ java -version java version "1.8.0_144" Java(TM) SE Runtime Environment (build 1.8.0_144-b01) Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
サンプルコードはこちらにあります。
OpenAPI の定義ファイルは、Synergy! メールAPI をベースにしたものを使っています。
OpenAPI Generator が生成するコードを変更したい場合、普通なら Github リポジトリ からソースコードを落としてきて、Java のコードを修正して Maven でビルドして..という感じになると思いますが、 Clone する手間や Maven をインストールしたりちょっと面倒です。
Groovy を使うことで、必要なクラスは @Grab を指定することで実行時に解決でき、Maven は不要になるので、Groovy を使います。私は普段 Mac で開発しているため、Homebrew で Groovy をインストールしました。
手始めとして、JavaClientCodegen を継承したクラスを実行する Groovy スクリプトを作ります。
@Grab(group = 'org.openapitools', module = 'openapi-generator-cli', version = '4.2.2') import org.openapitools.codegen.* import org.openapitools.codegen.languages.* class TechscoreJavaClientCodegen extends JavaClientCodegen { static main(String[] args) { OpenAPIGenerator.main(args) } TechscoreJavaClientCodegen() { super() } String name = "techscore-codegen" }
上記をファイル名 techscore-client-codegen.groovy として保存後、以下のコマンド実行で、output フォルダに Spring WebFlux のクライアントコードが生成されます。
groovy \ ./techscore-client-codegen.groovy \ generate \ -i ./openapi.yml \ -g TechscoreJavaClientCodegen \ -o ./output \ --library webclient
上記で生成されるクライアントコードは、前述したとおりそのままではビルドが通らない、アクセストークン発行のコードが生成されないなどといった状態です。
ビルドが通らないのは、生成される build.gradle に Spring 関係の依存関係について一切記載が無いことが原因です。
これを解消するために、OpenAPI Generator はテンプレートからコード生成するため、OpenAPI Generator のテンプレートをカスタマイズする必要があります。
Java のクライアントコードのテンプレートは、こちら から取得できます。
これをコピーして、ファイルを修正・追加することで生成されるコードを変更することができます。
以下のようにして、template ディレクトリ配下にコピーします。
mkdir -p {repodir,template} && cd repodir git init git remote add origin https://github.com/OpenAPITools/openapi-generator.git git config core.sparsecheckout true vi .git/info/sparse-checkout ← modules/openapi-generator/src/main/resources/Java/ を追記 git fetch origin git checkout v4.2.2 git pull origin v4.2.2 cp -pR modules/openapi-generator/src/main/resources/Java/* ../template/ cd ../ && rm -Rf repodir
template ディレクトリの中は以下のような構成になっています。拡張子 .mustcache は OpenAPI Generator が利用しているテンプレートエンジン Mustache のテンプレートファイルです。
$ tree -L 3 template template ├── auth │ ├── ApiKeyAuth.mustache │ ├── Authentication.mustache │ ├── HttpBasicAuth.mustache │ ├── HttpBearerAuth.mustache │ ├── OAuth.mustache │ └── OAuthFlow.mustache ├── libraries │ ├── feign │ (略) │ └── webclient │ ├── auth │ ├── ApiClient.mustache │ ├── README.mustache │ ├── api.mustache │ ├── api_test.mustache │ └── pom.mustache ├── ApiClient.mustache (略) ├── build.gradle.mustache ├── build.sbt.mustache (略) └── xmlAnnotation.mustache
クライアントコード生成時、--library webclient
(Spring WebFluxのコード生成)を指定すると、 libraries/webclient のテンプレートが使われることになります。
今回の場合、 libraries/webclient/build.gradle.mustache を作成すれば、build.gradle を変更できます。
build.gradle.mustache の内容はこちらを参照ください。
以下のように -t オプションを追加してテンプレート指定することでテンプレートの差し替えが可能です。
groovy \ ./techscore-client-codegen.groovy \ generate \ -i ./openapi.yml \ -g TechscoreJavaClientCodegen \ -o ./output \ -t ./template \ --library webclient
アクセストークン発行のコードを生成するためには、新しくテンプレートファイルを追加する必要があります。
今回は、Spring WebFlux のクライアントコードのため、WebClient を DI できるように、テンプレートを作成し libraries/webclient/WebClientConfig.mustache に登録しました。
Synergy! メールAPI ではアクセストークン発行時に scope と audience をリクエストボディに含める必要があるため、そのような実装をしています。また、アクセストークンはある程度キャッシュを利用するようにしています。実装内容はリンク先を参照いただければと思います。
また、ApiClient.mustache, api.mustache も、上記 WebClient を DI できるように少しずつ内容を変更しています。こちらも内容はリンク先を参照ください。
ただし、新しいテンプレートを追加しても、それだけではテンプレートを読み取ってくれません。Groovy に以下の Override メソッドを追加して、テンプレートを読み取るようにします。
@Override public void processOpts() { final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/"); final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/"); super.processOpts() if (WEBCLIENT.equals(getLibrary())) { // add WebClientConfig supportingFiles.add(new SupportingFile("WebClientConfig.mustache", invokerFolder, "WebClientConfig.java")) } }
先ほど触れませんでしたが、上記の WebClientConfig.mustache を元に生成される WebClientConfig.java は、Spring Boot の定義ファイルである application.yml から OAuth2 関連の設定内容を読み取ることを前提としています。(この辺りや、この辺りです。)
application.yml もテンプレートファイルが存在しないため、先ほど同様テンプレートファイルを作成して、それを読み取るコードを Groovy スクリプトに追記する必要があります。
{{#authMethods}}{{#isOAuth}} spring: security.oauth2.client: provider: paas: token-uri: https://auth.paas.crmstyle.com/oauth2/token registration: paas: authorization-grant-type: client_credentials client-id: your-client-id client-secret: your-client-secret scope: - mail:send - mail:result audience: https://mail.paas.crmstyle.com{{/isOAuth}}{{/authMethods}} logging: level: reactor.netty: DEBUG
@Override public void processOpts() { final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/"); final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/"); super.processOpts() if (WEBCLIENT.equals(getLibrary())) { // add WebClientConfig supportingFiles.add(new SupportingFile("WebClientConfig.mustache", invokerFolder, "WebClientConfig.java")) // add application.yml ★追記 supportingFiles.add(new SupportingFile("application.yml.mustache", projectFolder + '/resources', "application.yml")) } }
先ほどの application.yml のテンプレートは、アクセストークン発行 URL やスコープ、audience を固定で記載していました。(以下の★マークをつけている部分です。)
{{#authMethods}}{{#isOAuth}} spring: security.oauth2.client: provider: paas: token-uri: https://auth.paas.crmstyle.com/oauth2/token ★ここ registration: paas: authorization-grant-type: client_credentials client-id: your-client-id client-secret: your-client-secret scope: ★ここ - mail:send - mail:result audience: https://mail.paas.crmstyle.com ★ここ{{/isOAuth}}{{/authMethods}} logging: level: reactor.netty: DEBUG
どうせなら、OpenAPI の定義ファイルの SecuritySchemes から読み取った内容で置換したいです。
securitySchemes: mail_oauth: type: oauth2 description: mail auth flows: clientCredentials: tokenUrl: "https://auth.paas.crmstyle.com/oauth2/token" ★これに動的に差し替えたい scopes: ★これに動的に差し替えたい mail:send: "delivery" mail:result: "get the delivery results" x-audience: "https://mail.paas.crmstyle.com" ★これに動的に差し替えたい
アクセストークン発行 URL やスコープは、デフォルトの状態で OpenAPI 定義ファイルから内容を読み取ってくれます。
application.yml のテンプレートを以下のように変更することで、差し替えが可能です。
{{#authMethods}}{{#isOAuth}} spring: security.oauth2.client: provider: paas: token-uri: {{tokenUrl}} ★これで差し替え可能 registration: paas: authorization-grant-type: client_credentials client-id: your-client-id client-secret: your-client-secret scope: ★これで差し替え可能 {{#scopes}} - "{{scope}}" {{/scopes}} audience: https://mail.paas.crmstyle.com ★まだ置換できない {{/isOAuth}}{{/authMethods}} logging: level: reactor.netty: DEBUG
audience に関しては、 OpenAPI の標準仕様には含まれていない内容となり、Specification Extensions として定義する必要があるため、デフォルトでは OpenAPI Generator では読み取ってくれません。
OpenAPI Generator での SecuritySchemes の読み取りは、DefaultCodegen#fromSecurity が行なっています。
DefaultCodegen は、 JavaClientCodegen の一番祖先の継承元です。
内容を見ると、SecuritySchemes から読み取った内容を、CodegenSecurity というオブジェクトに詰めていっているように見えます。
/** * Convert map of OAS SecurityScheme objects to a list of Codegen Security objects * * @param securitySchemeMap a map of OAS SecuritySchemeDefinition object * @return a list of Codegen Security objects */ @SuppressWarnings("static-method") public List fromSecurity(Map<String, SecurityScheme> securitySchemeMap) { if (securitySchemeMap == null) { return Collections.emptyList(); } List codegenSecurities = new ArrayList(securitySchemeMap.size()); for (String key : securitySchemeMap.keySet()) { final SecurityScheme securityScheme = securitySchemeMap.get(key); CodegenSecurity cs = CodegenModelFactory.newInstance(CodegenModelType.SECURITY); cs.name = key; cs.type = securityScheme.getType().toString(); cs.isCode = cs.isPassword = cs.isApplication = cs.isImplicit = false; cs.isBasicBasic = cs.isBasicBearer = false; cs.scheme = securityScheme.getScheme(); if (SecurityScheme.Type.APIKEY.equals(securityScheme.getType())) { cs.isBasic = cs.isOAuth = false; cs.isApiKey = true; cs.keyParamName = securityScheme.getName(); cs.isKeyInHeader = securityScheme.getIn() == SecurityScheme.In.HEADER; cs.isKeyInQuery = securityScheme.getIn() == SecurityScheme.In.QUERY; cs.isKeyInCookie = securityScheme.getIn() == SecurityScheme.In.COOKIE; //it assumes a validation step prior to generation. (cookie-auth supported from OpenAPI 3.0.0) } else if (SecurityScheme.Type.HTTP.equals(securityScheme.getType())) { cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isOAuth = false; cs.isBasic = true; if ("basic".equals(securityScheme.getScheme())) { cs.isBasicBasic = true; } else if ("bearer".equals(securityScheme.getScheme())) { cs.isBasicBearer = true; cs.bearerFormat = securityScheme.getBearerFormat(); } } else if (SecurityScheme.Type.OAUTH2.equals(securityScheme.getType())) { cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isBasic = false; cs.isOAuth = true; final OAuthFlows flows = securityScheme.getFlows(); if (securityScheme.getFlows() == null) { throw new RuntimeException("missing oauth flow in " + cs.name); } if (flows.getPassword() != null) { setOauth2Info(cs, flows.getPassword()); cs.isPassword = true; cs.flow = "password"; } else if (flows.getImplicit() != null) { setOauth2Info(cs, flows.getImplicit()); cs.isImplicit = true; cs.flow = "implicit"; } else if (flows.getClientCredentials() != null) { setOauth2Info(cs, flows.getClientCredentials()); cs.isApplication = true; cs.flow = "application"; } else if (flows.getAuthorizationCode() != null) { setOauth2Info(cs, flows.getAuthorizationCode()); cs.isCode = true; cs.flow = "accessCode"; } else { throw new RuntimeException("Could not identify any oauth2 flow in " + cs.name); } } codegenSecurities.add(cs); } }
fromSecurity からさらに、 DefaultCodegen#setOAuth2Info という private メソッドが呼ばれています。
実装を見ると、OAuth Flow Object の内容を、CodegenSecurity に詰めているようです。
private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) { codegenSecurity.authorizationUrl = flow.getAuthorizationUrl(); codegenSecurity.tokenUrl = flow.getTokenUrl(); if (flow.getScopes() != null && !flow.getScopes().isEmpty()) { List<Map<String, Object>> scopes = new ArrayList<Map<String, Object>>(); int count = 0, numScopes = flow.getScopes().size(); for (Map.Entry<String, String> scopeEntry : flow.getScopes().entrySet()) { Map<String, Object> scope = new HashMap<String, Object>(); scope.put("scope", scopeEntry.getKey()); scope.put("description", escapeText(scopeEntry.getValue())); count += 1; if (count < numScopes) { scope.put("hasMore", "true"); } else { scope.put("hasMore", null); } scopes.add(scope); } codegenSecurity.scopes = scopes; } }
さらに読み進めると、OAuthFlow に getExtensions というメソッドがあり、
CodegenSecurity に、 vendorExtensions というプロパティが用意されていることが分かります。
Specification Extensions の内容を OAuthFlow#getExtensionsから読み取って、 CodegenSecurity の vendorExtensions にセットすれば、テンプレートファイルの置換が可能となるのではと推測できます。
実際に Groovy スクリプトを以下のように変更してみて試してみます。setOAuth2Info は private スコープなので、内容を DefaultCodegen からそのまま持ってきて getExtension するコードを追記しています。
fromSecurity が setOAuth2Info を呼ぶ作りになっているため、fromSecurity も内容は変更せずオーバーライドしています。
@Grab(group = 'org.openapitools', module = 'openapi-generator-cli', version = '4.2.2') import org.openapitools.codegen.* import org.openapitools.codegen.languages.* import io.swagger.v3.oas.models.security.*; class TechscoreJavaClientCodegen extends JavaClientCodegen { static main(String[] args) { OpenAPIGenerator.main(args) } TechscoreJavaClientCodegen() { super() } String name = "techscore-codegen" @Override public void processOpts() { final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/"); final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/"); super.processOpts() if (WEBCLIENT.equals(getLibrary())) { // add WebClientConfig supportingFiles.add(new SupportingFile("WebClientConfig.mustache", invokerFolder, "WebClientConfig.java")) // add application.yml supportingFiles.add(new SupportingFile("application.yml.mustache", projectFolder + '/resources', "application.yml")) } } @Override @SuppressWarnings("static-method") public List fromSecurity(Map<String, SecurityScheme> securitySchemeMap) { if (securitySchemeMap == null) { return Collections.emptyList(); } (略) return codegenSecurities; } private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) { codegenSecurity.authorizationUrl = flow.getAuthorizationUrl(); codegenSecurity.tokenUrl = flow.getTokenUrl(); if (flow.getScopes() != null && !flow.getScopes().isEmpty()) { List<Map<String, Object>> scopes = new ArrayList<Map<String, Object>>(); int count = 0, numScopes = flow.getScopes().size(); for (Map.Entry<String, String> scopeEntry : flow.getScopes().entrySet()) { Map<String, Object> scope = new HashMap<String, Object>(); scope.put("scope", scopeEntry.getKey()); scope.put("description", escapeText(scopeEntry.getValue())); count += 1; if (count < numScopes) { scope.put("hasMore", "true"); } else { scope.put("hasMore", null); } scopes.add(scope); } codegenSecurity.scopes = scopes; } // Specification Extensions をセット if (flow.getExtensions() != null) { Map<String, Object> extensions = flow.getExtensions(); codegenSecurity.vendorExtensions = extensions; } } }
次に application.yml のテンプレートファイルを以下のように変更します。
{{#authMethods}}{{#isOAuth}} spring: security.oauth2.client: provider: paas: token-uri: {{tokenUrl}} registration: paas: authorization-grant-type: client_credentials client-id: your-client-id client-secret: your-client-secret scope: {{#scopes}} - "{{scope}}" {{/scopes}} {{#vendorExtensions}} ★ 変更 audience: {{x-audience}}{{/vendorExtensions}}{{/isOAuth}}{{/authMethods}} logging: level: reactor.netty: DEBUG
この状態で Groovy を実行すると、SecuritySchemes から内容を読み取った src/main/resources/application.yml を得ることができます。
spring: security.oauth2.client: provider: paas: token-uri: https://auth.paas.crmstyle.com/oauth2/token registration: paas: authorization-grant-type: client_credentials client-id: your-client-id client-secret: your-client-secret scope: - "mail:send" - "mail:result" audience: https://mail.paas.crmstyle.com logging: level: reactor.netty: DEBUG
今回は Java + Spring WebFlux の例で OpenAPI Generator のクライアントコード生成のカスタマイズについて説明しました。ClientCodegen を継承したクラスを作り、テンプレートを修正・追記することで、他のサポートされている言語のクライアントコードも自分の好きなように変更することが可能と思います。(OpenAPI Generator のコードは読み解く必要がありますが)
皆さんも是非一度試してみてはいかがでしょうか。
The post OpenAPI Generator で OAuth2 アクセストークン発行のコードまで生成してみる first appeared on TECHSCORE BLOG.
]]>