【AWS】【Oracle Cloud】Lambda関数からOracle Autonomous Datawarehouse(ADW)に接続してみた


このマルチクラウド時代、なんか面白いことできないかなーと思って思いつきでやってみた系である。
要するに端を発するのは個人的好奇心であり、別になにか崇高な目的とか背景があったわけではない…
(まあこのブログ自体そんな感じだが…)


前提条件

  • 記載している情報は2020年6月時点のAWS及びOracle Cloudの画面や挙動に基づくものです。
    最新版だといろいろ違うかもしれませんのでご注意ください。
  • AWSもOracle Cloudも、利用可能なアカウントがすでに存在していることを前提として書いています。
  • デプロイパッケージの容量の関係でS3を使うので、S3に利用可能なバケットを作成しておく必要があります。

Oracle Autonomous Datawarehouseの作成

Oracle Cloud Infrastructureにサインインして、ハンバーガーメニューから「Autonomous Datawarehouse」を選択する。

「Autonomous Databaseの作成」を押す。

必要事項を入力・選択して「Autonomous Databaseの作成」を押す。
ADMINユーザーのパスワードは後々必要になるので忘れないようにする。


しばらくすると使用可能状態(Availableの直訳だろうな…)になるので「サービス・コンソール」を選択する。

「SQL Developer Web」を選択する。

適当にテーブルつくって適当にデータをいれる。
ここでは「TEST」という、ID NUMBER(10)、NAME VARCHAR2(100)だけの非常に簡易なテーブルを作り、3件データを投入。


一応以下のような感じです。

create table test(id number,name varchar2(100));  
  
insert into test values(1,'test1');  
insert into test values(2,'test2');  
insert into test values(3,'テスト三郎');  
  
select * from test;  

一度Oracle Cloudの管理コンソールに戻り、ADWのメニューから「DB接続」をクリックしてウォレットをダウンロードする。

ウォレットファイルはZIPファイルなので、適当な場所に保存して解凍する。
解凍した中にはいくつかファイルが入ってるが、今回必要になるのは「cwallet.sso」「sqlnet.ora」「tnsnames.ora」の3ファイル。
後で使うので覚えておこう(?)

Lambda関数の作成

個人的にやりやすかったというだけの理由でNode.jsのランタイムのLambda関数を使う。

Lambda関数のプロジェクト作成

作業はLinux環境下で行う。
自分も最初はWindows上で作業してLambdaにアップロードしてたのだが、どうも動作が怪しい(ちゃんと作ってるはずなのにエラーが出ることがあった)気がしたためである(勘違いの可能性も捨てきれない。wこの辺は詳細追えていません)
ちなみに私はAmazon LinuxのEC2たててそこで作業しました。

適当な作業場所をつくり、npm initでプロジェクト作成、npm install --save oracledbでnode-oracledbをインストールする。

Oracle Instant Client のダウンロード

OracleのサイトからOracle Instant Clientをダウンロードしてくる。
ダウンロードするのはLinux用である(LambdaがLinux上で動くっぽいため。動作インフラはAmazon Linuxなのかなやっぱり?)
また、Light版パッケージにする。LightじゃないとLambda関数のデプロイパッケージ制限にひっかかって最終的にデプロイできないからである。。。

Oracle Instant Clientの設置

プロジェクトディレクトリ直下にOracle Instant Client格納用のディレクトリを作成して、先ほどダウンロードしてきたインスタントクライアント一式を全部格納する。
ここでは「instantclient_19_6」というディレクトリ名でディレクトリを作成。(インスタントクライアント解凍時にできるディレクトリ名と同じ)

Instant Clientに含まれるファイルのうち、一部のファイルを削除する。
今回削除したのは以下の通り。

  • libclntsh.so.10.1
  • libclntsh.so.11.1
  • libclntsh.so.12.1
  • libclntsh.so.18.1
  • libocci.so.10.1
  • libocci.so.11.1
  • libocci.so.12.1
  • libocci.so.18.1

これはインスタントクライアント設置後の作業として確かどこかに載っていた(各バージョン別のファイルを削除する)のだが振り返っても見つけられなかった。
見つかったら追記します。多分。
上記のファイル群を削除すると以下のような見栄えになる。

要は 「libclntsh.so」「libocci.so」が最新バージョンのライブラリファイルへのシンボリックリンクになっていればそれで良い。

libaio.so.1を設置

プロジェクトディレクトリ直下に「lib」というディレクトリを用意する。
このディレクトリに「libaio.so.1」というライブラリファイルを設置する必要がある。
LinuxにおいてOracleへの接続に必要になるライブラリのようで、これがないと接続できない。
ググると「yumれ」とかいう情報が普通に出てくる(例えばこれとか)が、Lambda実行に際してそんなことはさせたくない(っていうかできるの??)ので、このライブラリをLambdaのデプロイパッケージに混ぜ込むことにする。
(この文面読んでも分かる通り、Lambdaを動かしている土台のインフラにはどうもこのライブラリは入ってないようだ。後述する)

ちなみにAmazon Linuxだと/usr/lib64配下にある。

「libaio.so.1」が向いてるリンク先の実態「libaio.so.1.0.1」を一度「libaio.so.1」というファイル名でプロジェクトディレクトリ直下につくった「lib」ディレクトリ配下にコピーする。
続いて「lib」ディレクトリ配下に移動し、「libaio.so」という名前のシンボリックリンクを、先ほどコピーしてきた「libaio.so.1」に向けてつくる。
プロジェクトディレクトリを「lambda_test」とすると、以下のような感じ。

> cp -rp /usr/lib64/libaio.so.1.0.1 /tmp/lambda_test/lib/libaio.so  
> cd /tmp/lambda_test/lib  
> ln -s libaio.so.1 libaio.so  

「lib.aio.so.1」だけあれば事足りるかと思ってたら、拡張子に「1」のついてないやつ「libaio.so」がシンボリックリンクとして必要らしいのだ。

/network/admin/配下にウォレットファイルの情報を格納

先ほどADWのコンソール画面からダウンロードしてきたウォレットファイル解凍後のファイルのうち、上述した3ファイル「cwallet.sso」「sqlnet.ora」「tnsnames.ora」を、インスタントクライアントを設置したディレクトリ下の~/network/admin/配下に格納する。

設置した3ファイルのうち、tnsnames.oraには以下のように接続定義として3つ用意されている。
[データベース名]high、[データベース名]mediaum、[データベース名]_lowの3つである。

hogeadw_high = (description= ...)  
  
hogeadw_low = (description= ...)  
  
hogeadw_medium = (description= ...)

今回は"hogeadw_low"を使ってみることにする。

Lambda関数の実行部を作る

SELECT一発投げて結果を返すだけの非常に簡単な処理にする。
select.jsを以下のように作成。

const oracledb = require('oracledb');  
  
module.exports = async () => {  
    try {  
            let con = await oracledb.getConnection({  
            user : 'ADMIN',  
            password : '[ADW作成時に指定したADMINユーザーのパスワード]',  
            connectString: 'hogeadw_test'  
        });  
  
        let result = await con.execute(`select * from TEST`);  
        return result;  
    } catch(err) {  
        console.log('error happened');  
        throw err;  
    }  
};  

これを呼び出すindex.jsを以下のように作成する。
これをLambdaのハンドラーとして初期設定する。
(面倒くさいのでエラーハンドリングは完全にシカトするw)

const select = require('./select.js');  
  
exports.handler = async (event) => {  
  
    console.log('START');  
  
    let result = await select();  
  
    const response = {  
        statusCode: 200,  
        body: JSON.stringify(result)  
    };  
  
    console.log('END');  
  
    return response;  
  
};  

最終的な構成を整理

ここまでの作業で、以下のようなディレクトリ構成になっているはずである。

/lambda_test ←これがプロジェクトディレクトリとする  
    |-----/instantclient_19_6  
    |        |-----/network  
    |        |        |-----/admin  
    |        |              |-----tnsnames.ora、sqlnet.ora、cwallet.sso  
    |        |-----libclntsh.soとかその他大勢  
    |-----/lib  
    |        |-----libaio.so、libaio.so.1(libaio.soはlibaio.so.1へのリンク)  
    |-----/node_modules ← oracledbが入っている(さらにその下にいっぱいあるけど省略)  
    |-----index.js  
    |-----select.js  
    |-----package.json

これをZIP圧縮する。

> cd lambda_test  
> zip -r lambda.zip ./*  

S3経由でLambdaにアップロード

自分がやったときにはファイルサイズが約59Mになったので、S3にあげて使うことにする。
上で圧縮したlambda.zipをS3にアップロードし、オブジェクトURLを取得。

つくったLambda関数に入り、コードをS3からアップロードする。
先ほど取得したオブジェクトURLを貼り付けて「保存」。

環境変数を設定

続いて環境変数を設定する。
Linux環境下からOracle Instant Clientを使って実際にOracleに接続するときと同様に、LD_LIBRARY_PATHを設定する。
設定値は以下の通り。

環境変数名 設定値
LD_LIBARY_PATH /var/task/instantclient_19_6:/var/task/lib

どうもLambdaは実行時に/var/taskというディレクトリ配下に動作コードが配置されるらしい。
なお、これは何度かミスった結果、ログからこの情報を知ったというだけで、決して公式情報ではないというのを断っておきたい(というか公式にこの辺の情報あがるもんなのかどうか謎である)
今回で言えば、

  • インスタントクライアントの入っているinstantclient_19_6というディレクトリ
  • libaioの入ってるlibディレクトリ

を実行コードindex.jsと同階層に配置しているので、LD_LIBRARY_PATHをそこに通す。

テストしてみる

適当なテストイベントを作ってテスト実行してみる。
今回はINPUTとなるイベントオブジェクトの中身は一切使わないのでなんでもいい(テスト選択時に出てくる3つくらいの非常に簡単な値でいい)

ADWにつくったTESTテーブルに登録した3件がちゃんと返ってきていることが確認できた!

苦労話

割とさっくり書いてるが(?)、ここに至るまでに実際には結構いろいろ苦労している。
主要な苦労ポイントを以下にあげておく。

そもそもNode-oracledbが読み込めない

初っ端はこれだった。

NJS-045: cannot load a node-oracledb binary for Node.js 12.16.3 (linux x64) \n  
Looked for /var/task/node_modules/oracledb/build/Release/oracledb-4.2.0-linux-x64.node,  
           /var/task/node_modules/oracledb/build/Release/oracledb.node,  
           /var/task/node_modules/oracledb/build/Debug/oracledb.node,  
           /var/task/node_modules/oracledb/lib/node_modules/oracledb/build/Release/oracledb-4.2.0-linux-x64.node,  
           /var/task/node_modules/oracledb/lib/node_modules/oracledb/build/Release/oracledb.node\n  
           Node-oracledb installation instructions: https://oracle.github.io/node-oracledb/INSTALL.html\n

npm install oracledbしたモジュールが読み込めない、的なことで文句言われている。
正確に言うとnode_modules配下にあるバイナリが読み込めない、というようなことを言われている。

これはoracledbのモジュールをrequireしたとき(let oracledb = require('oracledb')とかしたとき)に起きる。
要するにoracledb自体が読み込めていなかった。

これはどうも環境変数の設定をミスってることに起因していたらしい。
後述するインスタントクライアントのライブラリを読み込ませるため、何個か試したのだが、PATH環境変数を/instantclient_19_6ディレクトリに通してしまった結果、nodeから見たときにoracledbのモジュールが見えなくなってしまったようだった。
まあそりゃそうだよな。。
今にして思えばPATHをLambdaの環境変数設定でいじくるというのが少し強引過ぎた感は確かにある。

余談だがこのエラーメッセージで「LambdaがNodeを実行するときは/var/task配下で実行してるんだ…」というのを知る。
最終的にLD_LIBRARY_PATHを設定する際に良いヒントになった。
(まあでもこれくらいなら__dirnameをconsole.logするだけでわかるっちゃわかるのか)

libclntsh.soがないよと文句を言われる

上のを解決したと思ったら今度はこんなエラーが発生した。

エラーメッセージは以下の通りである。

DPI-1047: Cannot locate a 64-bit Oracle Client library: \"libclntsh.so: cannot open shared object file: No such file or directory\". See https://oracle.github.io/odpi/doc/installation.html#linux for help\nNode-oracledb installation instructions: https://oracle.github.io/node-oracledb/INSTALL.html\nYou must have 64-bit Oracle client libraries in LD_LIBRARY_PATH, or configured with ldconfig.\nIf you do not have Oracle Database on this computer, then install the Instant Client Basic or Basic Light package from \nhttp://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html\n

libclntsh.soはインスタントクライアントに含まれるライブラリファイルである。
lambda関数のコード内で、わざわざfsモジュール読み込んでreadFileSyncしてみて、ちゃんとファイルアクセスできるのまで確認したのに、なんでないって言われなきゃいけねんだ?あん?と思ってた。

これは何度かいろんな理由で起きた。
一つは環境変数の設定ミスである。
LD_LIBARY_PATH=$PATH/instantclient_19_6とか設定していた。
多分$PATHの部分が正しく解釈されずにパスが通ってないと判断されたのだと思われる。
これに限らず、Lambdaの環境変数設定に一般的なLinuxの環境変数設定でよく?使う、違う環境変数との結合指示は、Linux環境での想定通りに動作しない。
いわゆる「変数」をLambdaの環境変数に入れる場合ってどうやってやればいいんだろうね?

環境変数のミスの流れで以下のようなミスもした。
Lambda側の環境変数を信じられなくなり、以下のようにコード内で無理やり環境変数を定義しようとした。

var path = require('path');  
process.env['LD_LIBRARY_PATH'] = path.join(__dirname, '/instantclient_19_6') + ':' + path.join(__dirname, '/lib') + ':' + process.env['LD_LIBRARY_PATH'];  

これもダメ。
コード実行時には環境変数が「それっぽく」なってるが(実際console.logとかで中身見るとその通りになってるのがわかる)、実際にはこうやって設定しても無視される。
無視されるというより、Lambda自体に設定した環境変数の設定のほうが優先されるため、このように小細工して環境変数を設定しても実際のところ環境変数として機能しない(パスが通らない)。

環境変数以外の要因だと、「libclntsh.so」という名前のシンボリックリンクが必要という意味でエラーが出ていたというのもあった。
最初、インスタントクライアント配下のライブラリファイル(シンボリックリンクのほう)を、v19のバージョンだけ残して全部削除し、最後に残ったバージョンファイを「libclntsh.so」にリネームして使っていたが、これだとダメ(libclntsh.soはシンボリックリンクとして残しておく必要がある)だったようだ。
AWSのDocumentにそれっぽい記述があり、これがヒントになって解決した。
https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_LargeDBs.SBS.configure-dms-agent-linux-host.oracle.html

libaio.soがないよと文句を言われる

libclntsh.soを設置していざ準備万端さあ今度こそ来い!と思って臨んだら、今度はこんなエラーが発生した。

エラーメッセージは以下の通りである。

DPI-1047: Cannot locate a 64-bit Oracle Client library: \"libaio.so.1: cannot open shared object file: No such file or directory\". See https://oracle.github.io/odpi/doc/installation.html#linux for help\nNode-oracledb installation instructions: https://oracle.github.io/node-oracledb/INSTALL.html\nYou must have 64-bit Oracle client libraries in LD_LIBRARY_PATH, or configured with ldconfig.\nIf you do not have Oracle Database on this computer, then install the Instant Client Basic or Basic Light package from \nhttp://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html\n

内容的には上とほぼ同じだが、libaioなんてライブラリはそもそもインスタントクライアントに含まれていないので、文句言われても仕方なかった。
しかしインスタントクライアントに含まれてないのだとするとどこからもってくりゃいいのだ?と最初は途方にくれた。
このlibaio、「Oracle libaio」とかでググると「yumりましょう」とか普通に言われるのだが、Lambdaでyumするのは何かこう違和感を感じて受け付けなかった。(実際、できるのだろうか?)
どっかから探しだしてインスタントクライアントと同じやり方でデプロイパッケージに詰め込もう、と思い至った。

Amazon Linuxにはlibaioが同梱されているというのはAWSのDocumentにも記載があり、これは見つけられた。
そこで、EC2をAmazon Linuxで立ち上げて、ルート配下をfindで探すという無茶なことをして、場所を特定。(これが上記の/usr/lib64である)
プロジェクトディレクトリ配下にlibディレクトリをつくって、そこに見つけたlibaio.soを放り込んだ。

が、放り込んだら今度は「libaio.soがありません」と言われる。
これも上のlibclnsh.soと同じで、本体に結び付くシンボリックリンクが必要だった。
Linuxのライブラリって、こういう、バージョン名が付いてるライブラリの実態ファイルに対するシンボリックリンクを貼るのが慣例になってるのだろうか?(確かに/usr/lib64/配下のlibaio.soもそうなっていた)
ともあれ、シンボリックリンクを設置して問題解決。

おわりに

とりあえずAWS Lambda→Oracle ADWのクラウド跨ぎが確認できた。
振り返ってみると言うても結局のところはLambdaをOracle DBにつなげるだけの話だったな、という感じか。(こうやって書くと上の「苦労話」が情けなく見えてくるがw)

今回、いろいろ勉強になったのは、Lambda側の仕様や機能かなと思う。
環境変数の使い方や/var/taskの話とかは「へえ~」って感じ(?)で、細かいところだがナレッジとして蓄積できて個人的によかった。
また、上には載せていないが、裏でAWS CLIを色々使っており、CLIを使ったLambdaの更新方法なんかも主要なところが理解できたのは自分にとって+になったと思っている。
一方でADWはまさしくRDBとしてのみ利用しており、ADWの機能は全く活用できていない。(RDBとして利用、というのすら言いすぎなレベルの気もするが)
例えばADWには接続元IPを指定するアクセス制御リストなるものがある(参考。これはATPの例だが、Autonomous Databaseという意味では同じ)ので、これと組み合わせて、LambdaのVPC設定を編集して特定のLambda関数からのみアクセスを許可するとか、そういうのも出来るのではないか、と構想している。
この辺は時間があればもう少し詰めたいところである。

しかし、LambdaをOracle ADW(などのクラウドDB)とつなげる、というのは実際のところどこまで需要があるものなのか、ちょっと疑問ではある。。
クラウドDBの前段にわざわざLambdaを介在させる用途が確かにあまり思いつかないな…という。
そもそもこの記事ももともとは大した目的もなく俺が思いつきで始めたものだからね。。
まあ俺が思いつかないだけで実際には需要あるのかもしれないが。
そういえばnode-oracledb-for-lambdaってプロジェクトもあるので、相手がクラウドDBかどうかはともかく、LambdaとOracleとの連携は一応ケースとしてはあるのかもしれない(まあこのプロジェクト2~3年前から更新止まってるのだがw)