【AWS】【Oracle Cloud】LambdaのIPを固定してOracle ADWに定期接続する処理をつくった(が、やめた)話


タイトルの通りである。
最終的に"が、やめた"で締めきってるので残念ながら結末はネガティブである。
まあ、自身の失敗談として同じことを考えてる人(あまりいないと思うが)に向けられたらと思う。


はじめに

個人でAWSとOracle Cloud(Allways Free)を使っており、Oracle CloudのほうではOracle Autonomous Datawarehouse(以後Oracle ADW)をいろんな用途で使用している(容量制限があるとはいえ、個人で一つOracle DBを完全無料で持てるというのはとても良い)
が、Oracle CloudのAllways FreeでのADWは、7日間アクセスがないと自動的に停止状態になってしまう。
なので、使いたいときに毎回起動させなきゃいけないのが面倒くさかった。
よって 「6日の間隔を空けて定期的につつく仕組みつくれば毎回起動しなくてもいんじゃね?」と考えた。
この「6日ごとにOracle ADWをつつく」処理をLambdaでつくろう、と思い至った。
その自己満足ソリューションの作成過程をまとめておきたい。

結果的に「やめる」ため、今思うと本当にただの自己満足になりさがってしまったが、まあクラウドサービスを跨っていろいろ勉強できたのは良い経験だったような気がするので、その知見の共有と、自身の作品の供養と、今後もし同じことをしたくなったときのために記録として残しておくことにする。

その前に

これをみて「なんでわざわざAWS側からつつくんだ…?Oracle Cloud内でなんとかしろよ」と思う人もいるだろう。
これはその通りなのだが、下記になるような理由があってAWSに処理実態を組み込んだ。

  1. 最初は実際、Oracle CloudにVMたてて、Cronしこんで、ADWを定期的につつけばいいやと考えていた。
    が、Oracle CloudのAllways FreeではNAT Gatewayが作れなかった(2020年6月くらいの東京リージョンの話である。今はできるかも。試してない)
    よってPrivateのVMからADWに接続できない。
    まあInternet Gatewayは作れたので、Publicから接続すれば出来たんだけど。。PublicのサーバにOCIやAWSの認証情報とか置くのも心理的にちと抵抗があった。
    あと付け加えるなら、Lambdaだと「定期的に処理実行」が実現できるので、OracleのFaaSを使って同じようなことできるなら勉強のためにそっちを試そうとも考えたのだが、Allways FreeではOracle FaaSは利用できないし、仮に利用できたとしても「定期的に」が実現できるかまではわからなかった(多分まだできない)
    まあとにかくこうした理由でOracle側でDBに接続する機構を用意するのを早々に諦めた。
  2. マルチクラウド時代なので(?)、他のクラウドサービスと連携させてみたかった。
    ちょうどちょっと前に「LambdaからOracle ADWに接続してみる」という完全興味本位の実験が成功したこともあって、これを上手い具合に有効活用できないか、と考えた。
    付け加えると、↑の記事の最後に「LambdaのIPをOracle ADWのアクセス制御リストに指定するとかできないかなあ」みたいなことを書いていて、それの追加挑戦もしたかった。

2.の理由がかなり大きい。
要するに自分の好奇心に負けたのだ。

なぜLambdaのIPを固定に

Oracle ADWにはアクセス制御リストなる機能があって、許可する接続元IPアドレスを指定できる。(後述)
EC2のセキュリティグループとほとんど同じのOracle DB版と思ってよい。
いくらなんでもPublicにおっぴろげじゃ気分良くないし、せっかくこの機能があるんなら使いたいよな、と思った。

しかしLambdaは実行時に一時的に実行基盤が決まるので、外部に出るときのIPアドレスは毎回変わる。
(といいつつ、なんらかのアドレス帯域みたいのはあるんじゃないのかと疑ってはいるが、ググっても探せなかった)
このためLambdaを接続元とする場合、「どのIPから来るかわからん」のでOracle ADWのアクセス制御リストが使えない。
よって、外部に出るときのLambdaのIPアドレスを固定化し、当該IPアドレスをアクセス制御リストに設定することにした。

というかそれ以前にそもそもよく考えてみるに

使いもしないDB(というかVMも含むシステムリソース全般)は、いくら「動かし続けていても延々無料」だといっても、セキュリティを考えれば必要な時以外は停止しておくべきだ。
仕事だといらなくなったVMやらDBやらって絶対消すのに、個人だと上述したような知的好奇心が変に絡んでくるせいで、前提と逆の施策をとってしまったりする。
よくない。
気を付けましょう。(?)←自分への戒めです。。

VPCの用意だに

外に出るIPが固定になっているPrivate Subnetをつくる。

  1. まずVPCのメニューにいって適当にVPCをつくる。 CIDRは10.0.0.0/16とする。

  2. 次にサブネットのメニューにいってサブネットをつくる。 後でNAT Gatewayをつくるため、Public Subnetを最低1つ、
    加えて、後でLambdaに設定するときに2つ以上のサブネットの指定を推奨されるため、Private SubnetをCIDR違いで2つつくる。

    これは自分の過去記事を流用させていただく。

    アベイラビリティ・ゾーン CIDR Subnet用途
    ap-northeast-1a 10.0.1.0/24 Public Subnet
    ap-northeast-1c 10.0.2.0/24 Public Subnet
    ap-northeast-1a 10.0.3.0/24 Private Subnet
    ap-northeast-1c 10.0.4.0/24 Private Subne
  3. Elastic IPのメニューにいって「Elastic IPの割り当て」ボタンを押してElastic IPを取得する。

  4. NAT GatewayのメニューにいってNAT Gatewayをつくる。
    この際、サブネットに↑の2.でつくったPublic Subnet、Elastic IP割り当てIDに↑の3.でつくったElastic IPを選択する。

  5. ルートテーブルのメニューにいってルートを作る。 ルートテーブルそのもの作成時点ではVPCを指定するのみ。

    そのあと一覧画面から「ルートの編集」をクリックしてルーティングの設定を編集する。

    0.0.0.0/0のターゲットを↑の4.でつくったNAT Gatewayを指定して保存する。

これで外に出るIPが固定になってるPrivate Subnetが作成できた。

Lambdaの用意だに

これは上記で引用した過去の自分の記事とほとんど同じなので省略する。
一応以下の構成になっている。

index.jsだに

Lambdaがコールするハンドラを持ってるコード。
処理の実態はselect.jsとsnspublish.jsに任せている。

const select = require('./select.js');  
const snsPublish = require('./snspublish.js');  
exports.handler = async (event) => {  
  
    console.log('START');  
  
        try {  
                let result = await select();  
                console.log('END:DB SELECT');  
                console.log(result);  
  
                let snsResp = await snsPublish(result);  
                console.log('END:SNS Publish');  
                console.log(snsResp);  
  
                let response = {  
                        statusCode: 200,  
                        body: JSON.stringify(result)  
                };  
  
                console.log('END');  
  
                return response;  
        } catch(err) {  
                console.log('error happened');  
                //throw err;  
                let response = {  
                        statusCode: 500,  
                        body: JSON.stringify(err)  
                };  
                return response;  
        }  
};  

select.jsだに

ADMINで接続してsysdateとってくるだけ。

const oracledb = require('oracledb');  
  
module.exports = async () => {  
    try {  
            console.log('START:CONNECTION');  
            let con = await oracledb.getConnection({  
            user : 'ADMIN',  
            password : '***',  
            connectString: '***'  
        });  
  
        console.log('START:SELECT');  
        //let result = await con.execute(`select * from TEST`);  
        let result = await con.execute(`select sysdate from dual` ,  
                [] ,  
                {  
                        outFormat: oracledb.OUT_FORMAT_OBJECT,  
                        'SYSDATE': {type: oracledb.STRING}  
                }  
        );  
        return result;  
    } catch(err) {  
        console.log('error happened');  
        throw err;  
    }  
};  

snspublish.jsだに

ADWをつついたことをAWSのSimple Notification Serviceを通じて俺自身に通知する。
「今日も元気です」という、魔女の宅急便みたいなメッセージを送ってくれる。

// Load the AWS SDK for Node.js  
var AWS = require('aws-sdk');  
  
var sns = new AWS.SNS({  
        apiVersion: '2010-03-31',  
        region: 'ap-northeast-1'  
});  
  
module.exports = async (selectResult) => {  
        let message = `Oracle ADWは元気みたいですよ。  
        取得したデータ:  
        ${selectResult.rows[0].SYSDATE}  
        `;  
        // Create publish parameters  
        var params = {  
          Message: message , /* required */  
          Subject: new Date().toLocaleDateString('ja') + 'の報告',  
          TopicArn: 'arn:aws:sns:ap-northeast-1:123456789012:xxx'  
        };  
  
        let snsPublishPromise = sns.publish(params).promise();  
        try {  
                let resp = await snsPublishPromise;  
                return resp;  
        } catch(err) {  
                console.log('error happened at sns publish');  
                console.log(err);  
                throw err;  
        }  
}  

そういう意味だと先にこのSNSを作っておいて、かつLambdaにこのSNSトピックをPublishできる権限の付与が必要である。
まあSNSのPublishは「6日ごとにOracle ADWに接続してOracle ADWを勝手に停止状態にしないようにする」という要件からするとただのオマケで、実際にこの処理が動いたときに「動いた」ことを自分が遠隔地であろうと知る手っ取り早い手段として用意してるだけなので、特別必要なければなくてもいい、かも。

LambdaをPrivate Subnetに設置だに

ここから先はLambdaのIP固定化の設定である。
ほとんどこれ
LambdaのVPC設定を編集してPrivate Subnetに設置する。

  1. の前に、Lambdaの実行ロールにEC2に属するいくつかのポリシー設定が必要である。
    「アクセス権限」のタブを開いて「実行ロール」のところに表示されているロールをクリックしてIAMを開く。

  2. 以下画像の3つのポリシーを付与する。

    ちなみに以下3つ

    • EC2:DescribeNetworkInterface
    • EC2:CreateNetworkInterface
    • EC2:DeleteNetworkInterface
  3. Lambda関数の詳細を開いてVPCの枠の「編集」を押す

  4. 「カスタムVPC」を選択し、VPC、サブネット(2つ以上選択することを推奨される)、セキュリティグループを選択する。
    このとき選ぶVPC、サブネットは↑でつくったやつら。

  5. 1.の実行ロール設定がちゃんとできてればこれで設定完了である。
    ちなみにポリシーが不足していると「このロールはその辺の権限持ってねえよ」といって怒られて編集完了できない。

Lambdaの起動トリガーを設定だに

今回は「6日ごとに動かしたい」だったのでEventBridgeをトリガーにしてrate式で設定した。
rate式の内容はrate(6 day)

このトリガー指定してしまうとこの場でLambdaが動き出すが、接続先であるOracle ADWのアクセス制御リストの設定が済んでないと接続できずに失敗する。
だから先に↓のOracle ADWのアクセス制御リストを設定しておいたほうがいいかもしれない。

Oracle ADWの設定だに

今度はOracle Cloud側に移る。

Oracle ADWを選択して「アクセス制御リスト」の「編集」リンクをクリックする

「IP表記タイプ」を「Ip Address」を選択し、IPアドレスに↑で取得したElastic IPを指定する。


ここまでで「6日ごとに動いてOracle ADWをつつくIP固定Lambda」を用意する手版は終わった。
あとは放っておけば勝手に6日ごとにADWをつついて延命措置を続けてくれる。


なんでやめるかというとだに

AWSのNAT Gatewayの利用料金がかかるから。
単純に知らなかったのだが(それがそもそも素人ということなのだろうが)、NAT Gatewayは 「置いておくだけで金がかかる」のだ。
この処理構成だと6日に1回、しかも長くても数秒程度「外(インターネット)」に出るだけなのに、残りの時間帯も含めて、ただそこに存在しているだけで利用料金を取られる。
というか「置いておくだけで金がかかる」+「実際に使ったら通信の内容に応じて追加の費用がかかる」というのが料金体系のようだ。
特に前者の「置いておくだけで金がかかる」のが曲者で、1日ずーっと放置しているだけで1.488$(2020年7月レートと実績による)かかる。
30日間で44.64$≒ 4687円!!(2020年8月のレートです)
あほくさ。。。
と思ってやめることにした。
月に4000円強払うくらいなら毎回Oracle ADW起動してから始める面倒くささを俺は選ぶ。

結局のところ料金体系をあまり知らないままで適当に使っていたら痛い目を見たという自身の反省の経験に基づく撤退である。
こうして振り返るとまあ結構なかっこ悪さ…
まあでもこういう失敗経験を経て人は強くなっていくのだ(?)
今回の経験を反省に、もう二度とこういうNAT Gatewayの使い方はしないと心に誓った。
っていうかもういいやNAT Gateway…サヨナラ。(ポチッ)

ちなみにAWSとOracle ADWの接続経路を今回のような単純インターネットではなくVPNにすることなどでもこのやり方は実現可能であろう。
その場合の費用感がどうなるのかはわかっていない。
まあ安くてももうやる気はしないが。