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サンプルを動かしてみた。 を参考に 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への接続パスワードと接続用ウォレット内の各ファイルごとにシークレットを作成します。
- ボールトの作成
- キーの作成
- ウォレット内の各ファイルとDBユーザのパスワードをBase64でエンコード
- 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"
参考
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を追記
<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ファイルにタイムアウトとメモリの値を追記
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に接続することができた。