[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が利用できるように以下の作業を実施

- 仮想クラウド・ネットワークの作成
- IAMポリシーの作成
- (オプション) OCIレジストリの作成
- [Cloud Shellを使ってOracle Functionsサンプルを動かしてみた。](https://qiita.com/kenwatan/items/d8fd2426e93a1cfb65f6) を参考に fn shell のセットアップを実施。

接続する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コンソールから[セキュリティ] > [ボールト] で「ボールトの作成」を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108635/b95d0961-d3a5-8826-62b6-6bb799f2a3ce.png)

#### キーの作成
作成したボールトに対して「キーの作成」からキーを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108635/6002e752-65de-0fe7-0667-c41ba3cc3950.png)

#### ウォレット内の各ファイルと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でエンコードした各ファイルごとのシークレットを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108635/311e70f0-128d-0eae-c5d0-acbc5a221d4b.png)

- 名前・説明を入力
- 暗号化キー：作成したキー
- シークレット・タイプ：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 ('scott@example.com','SCOTT','SALES');
commit;
```

#### ファンクションの作成/テストディレクトリの削除
すべてのファイルは [github](https://github.com/kenwatan/functions_ADB_Java_Secrets) より取得可能

```
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":"scott@abc.com","EMP_NAME":"SCOTT","EMP_DEPT":"SALES"}]

$ fn invoke oci-adb-jdbc-java-app oci-adb-jdbc-java-secrets |jq
[
  {
    "EMP_NAME": "SCOTT",
    "EMP_EMAIL": "scott@abc.com",
    "EMP_DEPT": "SALES"
  }
]
```

# おわりに

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

# 参考情報

- [Oracle Functions - Connecting To An ATP Database With A Wallet Stored As Secrets](https://blogs.oracle.com/developers/oracle-functions-connecting-to-an-atp-database-with-a-wallet-stored-as-secrets)
- [Cloud Shellを使ってOracle Functionsサンプルを動かしてみた。](https://qiita.com/kenwatan/items/d8fd2426e93a1cfb65f6)
- [Oracle Functionsことはじめ](https://oracle-japan.github.io/paasdocs/documents/faas/oraclefunctions/handson/getting-started/)
- [Autonomous Database ハンズオンラボ（ADB HOL）](https://community.oracle.com/docs/DOC-1034727)
- [OCIシークレットを使ってシェルスクリプトからOracle Databaseに接続してみた](https://qiita.com/nakaie/items/5eb9498268c6f1e49169)