[OCI]Oracle Functions: Autonomous Databaseとの対話をより簡単にする方法 (2019/08/06)

Oracle Functions: Autonomous Databaseとの対話をより簡単にする方法 (2019/08/06)

https://blogs.oracle.com/developers/oracle-functions-an-easier-way-to-talk-to-your-autonomous-database
投稿者:Todd Sharp

先週、Oracle FunctionsをAutonomous DBインスタンスに接続してデータをクエリし、永続化する方法について投稿しました。
この記事では、カスタムDockerfileを作成し、サーバーレスファンクションのデプロイと呼び出しに使用するDockerイメージにATPウォレットが含まれていることを確認します。

Autonomous DBに初めて触れる方は、私の「Complete Guide To Getting Up And Running With Autonomous DB In The Cloud」をご覧ください。

この方法では、Oracle REST Data Services (ORDS)を利用して、ATPインスタンスのシンプルなユーザーテーブルからデータを取得します。
ORDSについてはマイクロサービスシリーズで詳しく説明しましたが、もし見逃してしまった場合は、ORDSを有効にして、
ORDSを呼び出してデータを取得するためのシンプルなサーバーレスファンクションを作成する方法をもう一度見てみましょう。

ORDSの有効化


最初のステップは、これから扱うスキーマをRESTで有効にすることです。
これは以下のクエリで実現できるので、お気に入りのクエリエディタやCLIツール、SQL Developer Webを開いて以下のように実行します。

BEGIN
/* enable ORDS for schema */
ORDS.ENABLE_SCHEMA(p_enabled => TRUE,
p_schema => 'USERSVC',
p_url_mapping_type => 'BASE_PATH',
p_url_mapping_pattern => 'usersvc',
p_auto_rest_auth => TRUE);
COMMIT;
END;


注意:引数p_auto_rest_authをTRUEに設定すると、/metadata-catalogエンドポイントが公開されないように保護されます。

呼び出しのための認証トークンを生成できるように、必要な権限を作成してみましょう。

DECLARE
l_roles OWA.VC_ARR;
l_modules OWA.VC_ARR;
l_patterns OWA.VC_ARR;
BEGIN
l_roles(1) := 'SQL Developer';
l_patterns(1) := '/users/*';
ORDS.DEFINE_PRIVILEGE(
p_privilege_name => 'rest_privilege',
p_roles => l_roles,
p_patterns => l_patterns,
p_modules => l_modules,
p_label => '',
p_description => '',
p_comments => NULL);
COMMIT;

END;


特権に関連付けられた OAUTH クライアントを作成します。

BEGIN
OAUTH.create_client(
p_name => '[Descriptive Name For Client]',
p_grant_type => 'client_credentials',
p_owner => '[Owner Name]',
p_description => '[Client Description]',
p_support_email => '[Email Address]',
p_privilege_names => 'rest_privilege'
);

COMMIT;
END;


クライアントアプリケーションにSQL Developerのロールを付与します。

BEGIN
OAUTH.grant_client_role(
p_client_name => 'Rest Client',
p_role_name => 'SQL Developer'
);
COMMIT;
END;


これで client_id と client_secret を取得できるようになりました。

SELECT id, name, client_id, client_secret
FROM user_ords_clients;


client_id と client_secret を使用して、REST 呼び出し用の認証トークンを生成することができます (これについては後ほど説明します)。
この時点では、スキーマはRESTが有効になっており、呼び出しを認証するための設定はすべて完了していますが、実際にはまだテーブルを公開していません。
テーブル全体をAuto-RESTで有効にすると、テーブル上のCRUD用のRESTエンドポイントのフルセットが得られます。

BEGIN
ORDS.enable_object(
p_enabled => TRUE,
p_schema => 'USERSVC',
p_object => 'USERS',
p_object_type => 'TABLE',
p_object_alias => 'users',
p_auto_rest_auth => FALSE);
COMMIT;
END;


この例では、ユーザー名ごとに単一のユーザーを取得するエンドポイントを作成してみましょう。

BEGIN
ORDS.define_service(
p_module_name => 'users',
p_base_path => 'users/',
p_pattern => 'user/:username',
p_method => 'GET',
p_source_type => ORDS.source_type_collection_item,
p_source => 'SELECT id, first_name, last_name, created_on FROM users WHERE username = :username OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY');
COMMIT;
END;


これで、このRESTエンドポイントにヒットしてユーザーを取得するためのOracle Functionを設定できました。
ORDSベースのURLが必要なので、Oracle Cloudダッシュボードにログインして、ATPインスタンスの詳細を表示します。
詳細ページで「Service Console」をクリックします。



Service Consoleで「開発」をクリックし、「SQL Developer Web」をクリックします。



SQL DeveloperのWeb urlは以下のようになっているはずです。

https://[random chars]-demodb.adb.us-phoenix-1.oraclecloudapps.com/ords/admin/_sdw/?nav=worksheet

ここからはORDSのベースURLを取得できるので、/adminの前にすべてコピーします。

https://[random chars]-demodb.adb.us-phoenix-1.oraclecloudapps.com/ords/

これは、ORDSサービスのベースURLです。

サーバーレスファンクションの作成


Oracle Functionsに慣れている場合は、このステップをスキップしてください。
Oracle Functionsに慣れていない場合は、まず最初に行う必要があるのは、ファンクションを格納する「アプリケーション」を作成することです。

fn create app
\--annotation oracle.com/oci/subnetIds='["ocid1.subnet.oc1.phx..."]'
\ords-demo

有効なサブネットIDを渡す必要があります。
私は通常、すべてのサーバーレスファンクション用にVCNを作成し、そのVCN内のサブネットの1つを選択します。
必要に応じて、Oracle Cloud InfrastructureでVCNを作成する方法についてのドキュメントを確認してください。

ボーナス・ヒント
papertrailapp.comにアカウントを登録して、ログの保存先を設定します。
次に、ファンクションを fn update app ords-demo -syslog-url tcp://logs3.papertrailapp.com:53136 でアップデートして、
ファンクションのコンソール出力をpapertrailのイベントビューアにログを記録できるようにします。

ファンクションに client_id と client_secret を取得するために、ファンクションにいくつかの設定変数を設定する必要があります。
fn config app [app name] [config key] [config value]で設定できます。



注意: 機密情報を含む設定変数は、常に暗号化する必要があります。その方法については、OCIでのKey Managementの使い方のガイドを参照してください。

では、ファンクションを作成します。

fn init --runtime java ords-demo-fn

生成されたプロジェクトをお気に入りのIDEで開き、HelloFunction.javaクラスを見てみましょう。
ベースURLを設定し、認証トークンを取得するためのプライベートファンクションを作成しましょう。
ここでは、アプリケーションの設定変数が環境プロパティとして利用可能であることに注意してください。

public class HelloFunction {

private final String ordsBaseUrl = "https://hvg9nd7xibsaegv-demodb.adb.us-phoenix-1.oraclecloudapps.com/ords/usersvc"; 
private final HttpClient httpClient = HttpClient.newHttpClient();

public String handleRequest(String input) {
String name = (input == null || input.isEmpty()) ? "world" : input;
return "Hello, " + name + "!";
}

private String getAuthToken() {
String authToken = "";
try {
Map<String, String> env = System.getenv();
for (String envName : env.keySet()) {
System.out.format("%s=%s%n", envName, env.get(envName));
}
String clientId = System.getenv().get("clientId");
String clientSecret = System.getenv().get("clientSecret");
String authString = clientId + ":" + clientSecret;
String authEncoded = "Basic " + Base64.getEncoder().encodeToString(authString.getBytes());
HttpRequest request = HttpRequest.newBuilder(new URI(this.ordsBaseUrl + "/oauth/token"))
.header("Authorization", authEncoded)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials"))
.build();
HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
ObjectMapper mapper = new ObjectMapper();
TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {};
HashMap<String, String> result = mapper.readValue(responseBody, typeRef);
authToken = result.get("access_token");
}
catch (URISyntaxException | IOException | InterruptedException e) {
e.printStackTrace();
}
return authToken;
}
}


ここでは、HttpClientをセットアップし、ORDSのベースURLを含む変数を設定し、認証トークンを生成するために使用できるファンクションを作成しました。
ここで、ユーザを表す静的な内部クラスを追加してみましょう。

public class HelloFunction {

private final String ordsBaseUrl = "https://hvg9nd7xibsaegv-demodb.adb.us-phoenix-1.oraclecloudapps.com/ords/usersvc";
 private final HttpClient httpClient = HttpClient.newHttpClient();

public static class User {
public String id;
public String username;
@JsonAlias("first_name")
public String firstName;
@JsonAlias("last_name")
public String lastName;
@JsonAlias("created_on")
public Date createdOn;
@JsonIgnore
public List links;
}

/* removed other functions for brevity */
}


Oracle Functions Java FDK には Jackson が含まれているので、User クラスに @JsonAlias をアノテーションして、
ORDS JSON レスポンス内の特定の要素をレスポンスオブジェクト内のプロパティにマッピングしたり、
@JsonIgnore を使用して特定のプロパティがレスポンスに含まれないようにしたりすることができます。
この依存関係を pom.xml に含めてください。

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9</version>
<scope>compile</scope>
</dependency>


最後に、handleRequestメソッドを実装し、ORDSを呼び出してユーザ名でユーザを取得し、レスポンスをUserオブジェクトとしてシリアライズして返すことができます。
クラス全体を完全に実装すると、以下のようになります。

package com.example.fn;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.*;

public class HelloFunction {

private final String ordsBaseUrl = "https://hvg9nd7xibsaegv-demodb.adb.us-phoenix-1.oraclecloudapps.com/ords/usersvc";
 private final HttpClient httpClient = HttpClient.newHttpClient();

public static class User {
public String id;
public String username;
@JsonAlias("first_name")
public String firstName;
@JsonAlias("last_name")
public String lastName;
@JsonAlias("created_on")
public Date createdOn;
@JsonIgnore
public List links;
}

public User handleRequest(String username) {
User user = null;
try {
HttpRequest request = HttpRequest.newBuilder( new URI( this.ordsBaseUrl + "/users/user/" + username ) )
.header("Authorization", "Bearer " + getAuthToken())
.GET()
.build();
HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if( response.statusCode() == HttpURLConnection.HTTP_NOT_FOUND ) {
System.out.println("User with username " + username + " not found!");
}
else {
user = new ObjectMapper().readValue(response.body(), User.class);
}
}
catch (URISyntaxException | IOException | InterruptedException e) {
e.printStackTrace();
}
return user;
}

private String getAuthToken() {
String authToken = "";
try {
Map<String, String> env = System.getenv();
for (String envName : env.keySet()) {
System.out.format("%s=%s%n", envName, env.get(envName));
}
String clientId = System.getenv().get("clientId");
String clientSecret = System.getenv().get("clientSecret");
String authString = clientId + ":" + clientSecret;
String authEncoded = "Basic " + Base64.getEncoder().encodeToString(authString.getBytes());
HttpRequest request = HttpRequest.newBuilder(new URI(this.ordsBaseUrl + "/oauth/token"))
.header("Authorization", authEncoded)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials"))
.build();
HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
ObjectMapper mapper = new ObjectMapper();
TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {};
HashMap<String, String> result = mapper.readValue(responseBody, typeRef);
authToken = result.get("access_token");
}
catch (URISyntaxException | IOException | InterruptedException e) {
e.printStackTrace();
}
return authToken;
}
}


ファンクションのテスト


ファンクションが期待通りに動作することを確認するために、簡単なテストを書いてみましょう。
ここでは期待される JSON レスポンスをハードコードしていますが、あなたの場合は異なるでしょう。
テストを実行する前にシェルで clientId と clientSecretas の環境変数を設定しておく必要があります。

package com.example.fn;

import com.fnproject.fn.testing.FnResult;
import com.fnproject.fn.testing.FnTestingRule;
import org.junit.Rule;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class HelloFunctionTest {

@Rule
public final FnTestingRule testing = FnTestingRule.createDefault();

@Test
public void shouldReturnUser() {
testing.givenEvent().withBody("tsharp").enqueue();
testing.thenRun(HelloFunction.class, "handleRequest");

FnResult result = testing.getOnlyResult();
assertEquals("{\"id\":\"8C561D58E856DD25E0532010000AF462\",\"username\":\"tsharp\",\"firstName\":\"todd\",\"lastName\":\"sharp\",\"createdOn\":1561649500385}", result.getBodyAsString());
}

}


ファンクションのデプロイ


Fn CLIを使ってファンクションをクラウドにデプロイすると、ビルドプロセスの一部としてユニットテストが実行されます。
手動でテストを実行して合格を確認し、テストを実行するために環境に設定されている設定変数に依存しているので、
ビルドプロセスの一部としてテストの実行をスキップする独自のDockerfileをプロジェクトのルートにドロップする予定です
(余談:将来的にこのプロセスをより簡単にするために、PMやエンジニアと協力しています)。

FROM fnproject/fn-java-fdk-build:jdk11-1.0.98 as build-stage
WORKDIR /function
ENV MAVEN_OPTS -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Dhttp.nonProxyHosts= -Dmaven.repo.local=/usr/share/maven/ref/repository

ADD pom.xml /function/pom.xml
RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=true", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target", "--fail-never"]

ADD src /function/src

RUN ["mvn", "package", "-DskipTests=true"]
FROM fnproject/fn-java-fdk:jre11-1.0.98
WORKDIR /function
COPY --from=build-stage /function/target/*.jar /function/app/

CMD ["com.example.fn.HelloFunction::handleRequest"]


このファンクションで使用する標準のDockerfileに変更があるのは、最終ビルドのRUNコマンドに-DskipTests=trueを追加したことだけです。
これで、ファンクションをクラウドにデプロイするには fn deploy --app ords-demo を実行します。

ファンクションの呼び出し


この時点では,ファンクションは fn invoke ords-demo ords-demo-fn を実行することで呼び出すことができますが、
ユーザ名をファンクションに渡したいので, echo "tsharp" | fn invoke ords-demo ords-demo-fn を実行して,ユーザのレスポンスを受け取ります。



まとめ

この記事では、Autonomous DBインスタンスのスキーマに対してORDSを有効にし、ORDSを特権で保護し、
RESTエンドポイントへのHTTP呼び出しを認証するために使用できる認証クライアントとクレデンシャルを作成する方法を見てきました。
私たちは、サーバーレスファンクションからカスタム ORDS エンドポイントへの HTTP 呼び出しを行うためにこれらの資格情報を使用し、
その結果を Java POJO としてシリアライズし、ファンクション呼び出しからその結果を返しました。

Photo by Spring Fed Images on Unsplash

コメント

このブログの人気の投稿

Oracle Database 19cサポート・タイムラインの重要な更新 (2024/11/20)

Oracle APEXのInteractive Gridで、Oracle Formsと比較して、重複行の検証を制御/通過させる方法 (2022/07/21)

Oracle APEX 24.1の一般提供の発表 (2024/06/17)