Help us understand the problem. What is going on with this article?

[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コンソールから[セキュリティ] > [ボールト] で「ボールトの作成」を選択
image.png

キーの作成

作成したボールトに対して「キーの作成」からキーを作成します。
image.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

  • 名前・説明を入力
  • 暗号化キー:作成したキー
  • シークレット・タイプ: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 より取得可能

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に接続することができた。

参考情報

kenwatan
Database 好き
oracle
Oracle Cloudは、最先端の機能をSoftware as a Service、Platform as a ServiceおよびInfrastructure as a ServiceおよびData as a Serviceとして提供します。
https://cloud.oracle.com/ja_JP/home
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした