[OCI] OCIシークレットを使ってOracle FunctionsからAutonomous DBに接続してみた。


Oracle FunctionsでAutonomous Databaseに接続する際に、OCIシークレットを使用するFunctionを動かしてみた。

※ Oracle Functionsとは、Oracle Cloud Infrastructureで提供されるFn Projectのマネージドサービス

OCI Vault、OCIシークレットについて

Oracle Cloud Infrastructure Vault を使用すると、データを保護する暗号化キーと、
リソースに安全にアクセスするために使用するシークレットの資格情報を一元的に管理できます。

Vaultは、マスター暗号化キーとシークレットを安全に保存します。

OCI Vaultでは仮想プライベート・ボールトと仮想ボールトが選択でき、仮想ボールトタイプを利用する場合は、作成したキーバージョン数に応じて支払いを行い、その月の利用分は月末に請求されます。ただし、月あたり20バージョンまでは無償で利用可能となっています。

事前準備

Oracle Functionsが利用できるように以下の作業を実施

接続するAutonomous DBを作成し、接続用ウォレットをダウンロード

シークレット操作用のポリシーの割り当て

OCI シークレットの使用を許可するためにテナンシーレベルに以下のサービスレベルポリシーを適用

allow service VaultSecret to use vaults in tenancy
allow service VaultSecret to use keys in tenancy

操作するOCIユーザが所属するグループに以下のポリシーを適用

allow group [group] to manage vaults in tenancy
allow group [group] to manage keys in tenancy

Oracle Functionsがシークレット操作が可能になるようにリソースプリンシパルの構成

動的グループの作成
Functionsに使用するコンパートメント用に動的グループを作成
一致ルール

ALL{resource.type='fnfunc', resource.compartment.id='ocid1.compartment.oc1..aaaaaaaa ..."}

動的グループにポリシーの割り当て
シークレットを読み取れるように、動的グループにポリシーを割り当て

allow dynamic-group XXX  to read secret-family in tenancy

コンパートメントレベルでのポリシーでも可

シークレットの作成

Autonomous DBへの接続パスワードと接続用ウォレット内の各ファイルごとにシークレットを作成します。

  1. ボールトの作成
  2. キーの作成
  3. ウォレット内の各ファイルとDBユーザのパスワードをBase64でエンコード
  4. Base64でエンコードした各ファイルごとにシークレットを作成

ボールトの作成

OCI Webコンソールから[セキュリティ] > [ボールト] で「ボールトの作成」を選択

キーの作成

作成したボールトに対して「キーの作成」からキーを作成します。

ウォレット内の各ファイルとDBユーザのパスワードをBase64でエンコード

Cloud Shell など base64 コマンドが実行できる端末でAutonomous DBに接続用のウォレット内の各ファイルをBase64でエンコードします。あわせて、DBユーザのパスワードを記載したテキストファイル(password.txt)もエンコードします。

base64 -i cwallet.sso >  cwallet,sso.base64
base64 -i ewallet.p12 >  ewallet.p12.base64
base64 -i keystore.jks >  keystore.jks.base64
base64 -i ojdbc.properties >  ojdbc.properties.base64
base64 -i sqlnet.ora >  sqlnet.ora.base64
base64 -i tnsnames.ora >  tnsnames.ora.base64
base64 -i truststore.jks >  truststore.jks.base64
base64 -i password.txt >  password.txt.base64

Base64でエンコードした各ファイルごとにシークレットを作成

2 で作成したキーにBase64でエンコードした各ファイルごとのシークレットを作成します。

  • 名前・説明を入力
  • 暗号化キー:作成したキー
  • シークレット・タイプ:Base64
  • シークレット・コンテンツ:Base64でエンコードしたファイルの中身をペースト

作成したシークレットごとにOCIDを記録します。

ファンクションの作成

アプリケーションの作成

Webコンソールまたは、CLIでアプリケーションを作成

fn create app oci-adb-jdbc-java-app --annotation oracle.com/oci/subnetIds='["ocid1.subnet.oc1.phx..."]'

アプリケーションに構成を追加

  • シークレットのOCID
fn config app oci-adb-jdbc-java-app CWALLET_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app EWALLET_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app KEYSTORE_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app OJDBC_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app SQLNET_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app TNSNAMES_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app TRUSTSTORE_ID ocid1.vaultsecret.oc1.iad..
  • ADBへの接続用のDBユーザ名・パスワードのシークレットOCID・接続文字列 [user] と [tns_name] を環境にあわせて変更します。
fn config app oci-adb-jdbc-java-app DB_USER [user]
fn config app oci-adb-jdbc-java-app PASSWORD_ID ocid1.vaultsecret.oc1.iad...
fn config app oci-adb-jdbc-java-app DB_URL jdbc:oracle:thin:\@[tns_name]]\?TNS_ADMIN=/tmp/wallet
  • 実行するSQL文
fn config app oci-adb-jdbc-java-app SQL_TEXT "select * from employees"

参考

employees表の作成例.sql
CREATE TABLE EMPLOYEES (
    EMP_EMAIL VARCHAR2(100 BYTE) NOT NULL, 
    EMP_NAME VARCHAR2(100 BYTE),
    EMP_DEPT VARCHAR2(50 BYTE), 
    CONSTRAINT PK_EMP PRIMARY KEY ( EMP_EMAIL )
);

insert into employees values ('[email protected]','SCOTT','SALES');
commit;

ファンクションの作成/テストディレクトリの削除

すべてのファイルは github より取得可能

fn init --runtime java oci-adb-jdbc-java-secrets
cd oci-adb-jdbc-java-secrets
rm -r src/test/

pom.xmlファイルのdependenciesにJDBCやSDKを追記

pom.xml
        <dependency>
            <groupId>com.oracle.ojdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>19.3.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.oracle.oci.sdk</groupId>
            <artifactId>oci-java-sdk-vault</artifactId>
            <version>1.15.3</version>
        </dependency>
        <dependency>
            <groupId>com.oracle.oci.sdk</groupId>
            <artifactId>oci-java-sdk-secrets</artifactId>
            <version>1.15.3</version>
        </dependency>
        <dependency>
            <groupId>com.oracle.oci.sdk</groupId>
            <artifactId>oci-java-sdk-common</artifactId>
            <version>1.15.3</version>
        </dependency>
        <dependency>
            <groupId>com.sun.activation</groupId>
            <artifactId>jakarta.activation</artifactId>
            <version>1.2.1</version>
        </dependency>

func.yamlファイルにタイムアウトとメモリの値を追記

func.yaml
memory: 1024
timeout: 120

HelloFunction.javaの編集(一部抜粋)

クラス群のインポート

import com.fasterxml.jackson.core.JsonProcessingException;
import com.oracle.bmc.Region;
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider;
import com.oracle.bmc.secrets.SecretsClient;
import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails;
import com.oracle.bmc.secrets.requests.GetSecretBundleRequest;
import com.oracle.bmc.secrets.responses.GetSecretBundleResponse;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

変数の宣言

ファンクションのDockerイメージにウォレットファイルを保存するパス(/tmp)を宣言

private final File walletDir = new File("/tmp", "wallet");

DBのユーザー名とURL、実行するSQL文をファンクションの構成から値を取得

private final String dbUser = System.getenv().get("DB_USER");
private final String dbUrl = System.getenv().get("DB_URL");
private final String sqlText = System.getenv().get("SQL_TEXT");

デコードされたパスワード

private String dbPassword;

シークレットをクライアントに格納するために使用する変数

private SecretsClient secretsClient;

すべてのウォレットファイルのOCIDを保存するマップ

    private final Map<String, String> walletFiles = Map.of(
            "cwallet.sso",  System.getenv().get("CWALLET_ID"),
            "ewallet.p12",  System.getenv().get("EWALLET_ID"),
            "keystore.jks",  System.getenv().get("KEYSTORE_ID"),
            "ojdbc.properties",  System.getenv().get("OJDBC_ID"),
            "sqlnet.ora",  System.getenv().get("SQLNET_ID"),
            "tnsnames.ora",  System.getenv().get("TNSNAMES_ID"),
            "truststore.jks", System.getenv().get("TRUSTSTORE_ID")
    );

コンストラクタを作成

secretsClient.setRegion では、利用するリージョンを指定します。
下記例では、US-ASHBURN-1 を指定
東京リージョンの場合は AP_TOKYO_1 を指定します。

    public HelloFunction() {
        String version = System.getenv("OCI_RESOURCE_PRINCIPAL_VERSION");
        BasicAuthenticationDetailsProvider provider = null;
        if( version != null ) {
            provider = ResourcePrincipalAuthenticationDetailsProvider.builder().build();
        }
        else {
            try {
                provider = new ConfigFileAuthenticationDetailsProvider("~/.oci/config", "DEFAULT");
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
        secretsClient = new SecretsClient(provider);
        secretsClient.setRegion(Region.US_ASHBURN_1);

        String dbPasswordOcid = System.getenv().get("PASSWORD_ID");
        dbPassword = new String(getSecret(dbPasswordOcid));
    }

ウォレットの復元

ウォレットファイルのマップをループして、ウォレットファイルをテンポラリディレクトリに書き出し

    private void createWallet(File walletDir) {
        walletDir.mkdirs();
        for (String key : walletFiles.keySet()) {
            try {
                writeWalletFile(key);
            }
            catch (IOException e) {
                walletDir.delete();
                e.printStackTrace();
            }
        }
    }

    private void writeWalletFile(String key) throws IOException {
        String secretOcid = walletFiles.get(key);
        byte[] secretValueDecoded = getSecret(secretOcid);
        try {
            File walletFile = new File(walletDir + "/" + key);
            FileUtils.writeByteArrayToFile(walletFile, secretValueDecoded);
            System.out.println("Stored wallet file: " + walletFile.getAbsolutePath());
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

シークレットの取得と復元

    private byte[] getSecret(String secretOcid) {
        GetSecretBundleRequest getSecretBundleRequest = GetSecretBundleRequest
                .builder()
                .secretId(secretOcid)
                .stage(GetSecretBundleRequest.Stage.Current)
                .build();
        GetSecretBundleResponse getSecretBundleResponse = secretsClient
                .getSecretBundle(getSecretBundleRequest);
        Base64SecretBundleContentDetails base64SecretBundleContentDetails =
                (Base64SecretBundleContentDetails) getSecretBundleResponse.
                        getSecretBundle().getSecretBundleContent();
        byte[] secretValueDecoded = Base64.decodeBase64(base64SecretBundleContentDetails.getContent());
        return secretValueDecoded;
    }

handleRequest()

    public List handleRequest() throws SQLException, JsonProcessingException {
        System.setProperty("oracle.jdbc.fanEnabled", "false");
        if( !walletDir.exists() ) {
            createWallet(walletDir);
        }

        DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
        Connection conn = DriverManager.getConnection(dbUrl,dbUser,dbPassword);
        Statement statement = conn.createStatement();
        ResultSet resultSet = statement.executeQuery(sqlText);
        List<HashMap<String, Object>> recordList = convertResultSetToList(resultSet);
        conn.close();
        return recordList;
    }

クエリで返された行をリストにして返す

    private List<HashMap<String,Object>> convertResultSetToList(ResultSet rs) throws SQLException {
        ResultSetMetaData md = rs.getMetaData();
        int columns = md.getColumnCount();
        List<HashMap<String,Object>> list = new ArrayList<HashMap<String,Object>>();
        while (rs.next()) {
            HashMap<String,Object> row = new HashMap<String, Object>(columns);
            for(int i=1; i<=columns; ++i) {
                row.put(md.getColumnName(i),rs.getObject(i));
            }
            list.add(row);
        }
        return list;
    }

ファンクションのデプロイと呼び出し

デプロイと呼び出し

fn deploy --app oci-adb-jdbc-java-app
fn invoke oci-adb-jdbc-java-app oci-adb-jdbc-java-secrets

呼び出し例

$fn invoke oci-adb-jdbc-java-app oci-adb-jdbc-java-secrets
[{"EMP_EMAIL":"[email protected]","EMP_NAME":"SCOTT","EMP_DEPT":"SALES"}]

$ fn invoke oci-adb-jdbc-java-app oci-adb-jdbc-java-secrets |jq
[
  {
    "EMP_NAME": "SCOTT",
    "EMP_EMAIL": "[email protected]",
    "EMP_DEPT": "SALES"
  }
]

おわりに

ADBへの接続用ウォレットをシークレットに格納し、Functionsでシークレットを使ってADBに接続することができた。

参考情報