2020-04-21に更新

AWS Lambdaでcx_Oracleを使ってOracleDBへ接続する

読了目安:14分

前書き

Pythonで書かれた Lambdaファンクションから、OracleDBへ接続する方法を記載します。

LambdaファンクションからMySQLへの接続はたくさん記事があるんだけど、OracleDBへの接続はあまり記事がなく、実現まで結構時間がかかったので、自分用の備忘録に。

結論

  • LambdaファンクションからOracleDBに接続する時には、cx_Oracleのモジュールだけではなく、Oracle Instant Clientのモジュールも、デプロイパッケージに入れてあげる必要がある。
  • すべてのモジュールを一つのデプロイパッケージに入れると、大きすぎてAWS Lambdaのインラインエディタで編集できなくなる。なので、cx_OracleとOracle Instant Clientのモジュールはレイヤに分けると良い。

手順

前提

2020年4月時点で、使用した環境は下記の通りです。
python3のインストールまで完了している状態で、後続の作業を開始します。

  • Amazon Linux 2(AWS EC2上で作業)
  • python3.7
  • cx_Oracle7.3
  • Oracle Instant Client 19.6

手順の流れ

  1. 必要なモジュールをインストールする
  2. デプロイパッケージを作る
  3. Lambdaファンクションを作成する
  4. レイヤ用のデプロイパッケージを作成する
  5. Lambdaファンクションにレイヤを適用する

1. 必要なモジュールをインストールする

Oracle Instant Client

まずは、Oracle Instant Client から。
最新版のOracle Instant Clientを公式サイトからダウンロードします。作業環境が Amazon Linux 2 なので、Linux用の64ビット版を使います。
* Oracle Instant Client
* Instant Client Downloads for Linux x86-64 (64-bit)

インストールしたのは、とりあえずbasicとsqlplusのふたつだけ。LambdaファンクションからのDB接続だけを考えるなら、sqlplusは不要かも。

$ sudo yum -y localinstall ./oracle-instantclient19.6-basic-19.6.0.0.0-1.x86_64.rpm
$ sudo yum -y localinstall ./oracle-instantclient19.6-sqlplus-19.6.0.0.0-1.x86_64.rpm

インストール完了後、SQL*PLUSでDBに接続できることを確認しておきます。
なお、Oracle Instant Clientを使用する分には、ORACLE_HOMEなどの設定は不要らしいです。

$ sqlplus <user>/<pass>@<hostname>:<port>/<sid>
SQL*Plus: Release 19.0.0.0.0 - Production on xxx xxx xx xx:xx:xx xxxx
Version 19.6.0.0.0
Copyright (c) 1982, 2019, Oracle.  All rights reserved.
Last Successful login time: xxx xxx xx xxxx xx:xx:xx +09:00
Connected to:
Oracle Database 12c Standard Edition Release 12.1.0.2.0 - 64bit Production

SQL> select TNAME from tab;
TNAME
--------------------------------------------------------------------------------
SAMPLE_EMP

SQL> select * from SAMPLE_EMP;
EMPNO      EMPNAME                                              GENDER_F
---------- -------------------------------------------------- ----------
0000000001 EMP_A                                                       1
0000000002 EMP_B                                                       2
0000000003 EMP_C                                                       1

SQL> quit
Disconnected from Oracle Database 12c Standard Edition Release 12.1.0.2.0 - 64bit Production

サンプルで作っておいたテーブルの中身が見えており、Oracle Instant Clientのインストールは完了です。

cx_Oracle

PythonでOracleDBに接続するために、cx_Oracleもインストールしておきます。

$ sudo pip3 install cx_oracle

EC2からcx_Oracleを使ってOracleDBへ接続する

ここまで完了したら、pythonでOracleDBへ接続できるようになっているはずなので、簡単なサンプルでOracleDBに接続できるか確認します。例えば下記のような感じ。ファイル名はdbconnect_oracle.pyとします。

import json
import cx_Oracle

def lambda_handler(event, context):
    # TODO implement

    # cx_oracleバージョン確認
    verOraCli = cx_Oracle.clientversion()
    print( verOraCli )

    # oracle接続
    print( "DB connect" )
    connection = cx_Oracle.connect("<user>", "<pass>", "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=<hostname>) (PORT=<port>))(CONNECT_DATA=(SID=<sid>)))")

    # select実行
    print( "select execute" )
    try:
        cursor = connection.cursor()

        sql = "select * from sample_emp"
        cursor.execute( sql )

        result = cursor.fetchall()
        print( result )

    finally:
        cursor.close()
        connection.close()

    return {
        'statusCode': 200,
        'body': json.dumps( 'cx_oracle clientversion = ' + str(verOraCli) )
    }

本来はハンドラの中でDB接続処理を行わない方が良いけど、まあとりあえずということでひとつ。早速実行してみます。

$ python3 -c "import dbconnect_oracle; dbconnect_oracle.lambda_handler(0,0)"
(19, 6, 0, 0, 0)
DB connect
select execute
[('0000000001', 'EMP_A', 1), ('0000000002', 'EMP_B', 2), ('0000000003', 'EMP_C', 1)]

Oracle Instant Client 19.6 を使用してOracleDBでselectを実行できていることが確認できました。

2. デプロイパッケージを作る

EC2上での動作は確認できたので、AWS Lambdaの関数として定義するためにデプロイパッケージを作成します。

デプロイパッケージ構成

Lambdaファンクションで外部ライブラリのimportを行う場合、外部ライブラリを含んだデプロイパッケージを作る必要があります。今回はさらに、cx_OracleがOracle Instant Clientを利用するので、Oracle Instant Clinentもデプロイパッケージに含める必要があります。

というわけで、目指すべき構成は下記の形。作業ディレクトリは/tmp/dbconnect_oracle_packとします。

/tmp/dbconnect_oracle_pack
 ├ dbconnect_oracle.py
 ├ cx_Oracle.cpython-37m-x86_64-linux-gnu.so
 ├ cx_Oracle-7.3.0.dist-info
 │  ├ INSTALLER
 │  ├ METADATA
 │  ├ RECORD
 │  ├ WHEEL
 │  └ top_level.txt
 └ lib
    ├ libaio.so -> ./libaio.so.1
    ├ libaio.so.1
    ├ libclntsh.so -> ./libclntsh.so.19.1
    ├ libclntsh.so.19.1
    ├ libclntshcore.so -> ./libclntshcore.so.19.1
    ├ libclntshcore.so.19.1
    ├ libipc1.so
    ├ libmql1.so
    ├ libnnz19.so
    ├ libocci.so -> ./libocci.so.19.1
    ├ libocci.so.19.1
    └ libociei.so

デプロイパッケージ作成

デプロイパッケージにcx_Oracleを含めるため、作業ディレクトリへcx_Oracleを持ってきます。作業ディレクトリへのインストールなので、今回はsudoは不要です。

$ pip3 install cx_Oracle -t /tmp/dbconnect_oracle_pack

Oracle Instant Clientのモジュールは、インストール先からコピーします。libaio.so.1だけは、/usr/lib64/から持ってきます。

なお、記事を書く際に再度確認したら、libaio.so.1はシンボリックリンクで、実体はlibaio.so.1.0.1でした。
下記の通りでも動作はしますが、気になる方は、libaio.so.1.0.1をコピーしてそこからシンボリックリンクを作成する方が良いかもしれません。

$ mkdir /tmp/dbconnect_oracle_pack
$ cp /usr/lib/oracle/19.6/client64/lib/libclntsh.so.19.1 /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib/oracle/19.6/client64/lib/libclntshcore.so.19.1 /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib/oracle/19.6/client64/lib/libipc1.so /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib/oracle/19.6/client64/lib/libmql1.so /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib/oracle/19.6/client64/lib/libnnz19.so /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib/oracle/19.6/client64/lib/libocci.so.19.1 /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib/oracle/19.6/client64/lib/libociei.so /tmp/dbconnect_oracle_pack/lib
$ cp /usr/lib64/libaio.so.1 /tmp/dbconnect_oracle_pack/lib

最初に挙げたデプロイパッケージ構成に不足している部分を、シンボリックリンクで作成します。
デプロイパッケージのサイズ上限は250MBのため、絞れるところは絞りましょう。

$ cd /tmp/dbconnect_oracle_pack
$ ln -s ./libaio.so.1 libaio.so
$ ln -s ./libclntsh.so.19.1 libclntsh.so
$ ln -s ./libclntshcore.so.19.1 libclntshcore.so
$ ln -s ./libocci.so.19.1 libocci.so

ここまで完了すると、デプロイパッケージ構成通りの状態となっているはずです。zip圧縮して、デプロイパッケージを作成します。圧縮後のファイルは70MB弱くらいになると思います。
シンボリックリンクを含んでいるので、--symlinksオプションを忘れないように。また、圧縮対象は/tmp/dbconnect_oracle_packディレクトリではなく、その配下であることに注意してください。

$ cd /tmp/dbconnect_oracle_pack
$ zip -r --symlinks dbconnect_oracle_sample.zip ./dbconnect_oracle.py ./cx_Oracle.cpython-37m-x86_64-linux-gnu.so ./cx_Oracle-7.3.0.dist-info ./lib

3. Lambdaファンクションを作成する

完成したデプロイパッケージ元にLambdaファンクションを作成します。

ですが、作成したデプロイパッケージが50MBを超えているため、Lambdaファンクションとして直接アップロードを行う事ができません。一旦S3上にアップロードした後、LambdaファンクションにS3からアップロードする形になります。なお、Lambdaファンクションのサイズ上限は諸々含めて250MB。cx_OracleとOracle Instant Client だけで200MBを超えていて、結構カツカツです。

Lambdaファンクションとしてのアップロードが完了すると、インラインエディタで確認ができる…のが通常なのですが、サイズが大きすぎてインラインエディタでは編集できない旨の警告が表示されます。実行はできるようなので、良しとしておきます。

デプロイパッケージが大きすぎてインラインコード編集を有効にできません

OracleDBに限らず、DBに接続するLambdaファンクションにはIPアドレスの付与が必要です。VPC設定を行い、VPC設定、サブネット、セキュリティグループを指定します。

また、初回実行に限り、そこそこの時間がかかります。デフォルトのタイムアウト値は3秒となっていますが、10秒程度に増やしておいたほうがよいでしょう。

上記の設定を完了したら、テスト実行を行います。サンプルは引数不要のイベントハンドラなので、空のテストイベントを作成してテストを行いましょう。

実行結果:成功

ヨシ! ちゃんと、Oracle Instant Client 19.6が使われているようです。
初回は時間がかかりましたが、2回目からは1秒かからずに実行されます。時間がかかるのは、IP付与によるものかしら。

4. レイヤ用のデプロイパッケージを作成する

当初の目的である、cx_Oracleを使ってOracleDBへ接続するLambdaファンクションの作成は、ここまでで実現できました。

ただ、インラインエディタでの編集ができないのはちょっといただけません。サイズが大きい原因はcx_OracleとOracle Instant Clientなので、この部分をcx_Oracleレイヤとして切り出せば良いのではないか?

cx_Oracleレイヤ用のデプロイパッケージ構成

AWS Lambda レイヤー

関数コードからランタイム依存関係を移動するには、それらをレイヤーに配置します。Lambda ランタイムには、関数コードを使用して、レイヤーに含まれるライブラリにアクセスできることを確認する、/optディレクトリのパスが含まれます。

ライブラリをレイヤーに含めるには、ランタイムでサポートされているいずれかのフォルダにそれらを配置します。
* Pythonpythonpython/lib/python3.8/site-packages(サイトディレクトリ)

レイヤ用のデプロイパッケージを作る場合、pyhon関連のライブラリはpython配下に、配置します。なので、cx_Oracle関連のモジュールはこちらに置きます。

AWS Lambda 環境変数の使用

予約されていない環境変数
(略)
LD_LIBRARY_PATH – システムライブラリのパス
(/lib64:/usr/lib64:$LAMBDA_RUNTIME_DIR:$LAMBDA_RUNTIME_DIR/lib:$LAMBDA_TASK_ROOT:$LAMBDA_TASK_ROOT/lib:/opt/lib)。

Oracle Instant Clientのモジュールについては、LD_LIBRARY_PATHで指定されたパスに配置する必要があります。Lambdaファンクションの設定で編集しても良いのですが、デフォルトで指定されたパス/opt/libがあるので、そちらを使いましょう。レイヤに含まれるライブラリは/opt配下に配置されるとのことなので、lib配下です。

というわけで、レイヤ用デプロイパッケージの構成は下記の通りになります。作業ディレクトリは/tmp/dbconnect_oracle_pack/layerとします。

/tmp/dbconnect_oracle_pack/layer
 ├ python
 │ ├ cx_Oracle.cpython-37m-x86_64-linux-gnu.so
 │ └ cx_Oracle-7.3.0.dist-info
 │     ├ INSTALLER
 │     ├ METADATA
 │     ├ RECORD
 │     ├ WHEEL
 │     └ top_level.txt
 └ lib
    ├ libaio.so -> ./libaio.so.1
    ├ libaio.so.1
    ├ libclntsh.so -> ./libclntsh.so.19.1
    ├ libclntsh.so.19.1
    ├ libclntshcore.so -> ./libclntshcore.so.19.1
    ├ libclntshcore.so.19.1
    ├ libipc1.so
    ├ libmql1.so
    ├ libnnz19.so
    ├ libocci.so -> ./libocci.so.19.1
    ├ libocci.so.19.1
    └ libociei.so

レイヤ用デプロイパッケージ作成

配置ができたらzip圧縮し、レイヤ用デプロイパッケージを作成します。ファイル名は何でも良いですが、oraclelib.zipとしてみました。

$ cd /tmp/dbconnect_oracle_pack/layer
$ zip -r --symlinks oraclelib.zip ./python ./lib

5. Lambdaファンクションにレイヤを適用する

レイヤを作るため、AWS Lambdaの左側メニューから「レイヤー」を選択し、レイヤの作成を行います。

先程と同様に、S3経由でアップロードする形になります。また、「互換性のあるランタイム」には、作業環境であるAmazon Linux 2にインストールしたのと同じ Python3.7 を指定します。

レイヤー作成設定

レイヤの作成が完了したら、Lambdaファンクションにレイヤを適用します。

作成したLambdaファンクションの画面から「レイヤーの追加」を行う事ができます。
Lambdaファンクションのランタイムを「Python3.7」にした上で、「レイヤーの追加」を行ってください。

選択できるレイヤーは、Lambdaファンクションで設定しているランタイムを「互換性のあるランタイム」で指定したレイヤーだけです。
関数にレイヤーを追加

こうしておくと、Lambdaファンクションそのものは、今回のサンプルのdbconnect_oracle.pyだけにしても大丈夫です。インラインコード編集も可能となります。

OK

動作も問題ありません。

後書き

一番わからなかったのは、Oracle Instant Client 19 だとどのlibをデプロイパッケージに含めればよいのか、ということです。今回記事に残そうとしたのも、それが要因です。

それにしても、Oracle Instant Client だけで200MBオーバーなんだけど、そのうちOracle Instant ClientだけでLambdaのサイズ制限を超えてしまうのではないだろうか。

参考サイト

ツイッターでシェア
みんなに共有、忘れないようにメモ

平

「平」という名前で、鏡音さんにたまに歌ってもらってました。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント