HelidonでOCIを楽々操作！！

# はじめに

この記事は、[Oracle Cloud Infrastructure Advent Calendar 2022 その2 Day 16](https://qiita.com/advent-calendar/2022/oci)の記事として書かれています。

皆さんは[Helidon](https://helidon.io/)というアプリケーションフレームワークをご存知でしょうか？

私の過去記事では何度か登場しているのですが、今回はそのHelidonを使うとOCIを楽々に操作できますよというお話をしたいと思います。

ちなみにHelidonはOracleが開発しているJava製のオープンソースのアプリケーションフレームワークであり、非常に軽量に扱うことができるので、マイクロサービスの開発に向いているフレームワークです。

ちなみに過去の記事では以下などで登場しています。

* [OCI APMでOKE上のコンテナアプリケーションをトレーシングしてみよう](https://qiita.com/takuya_0301/items/9c8295d3e7436b47a5e5#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E6%BA%96%E5%82%99)
* [OCI(Oracle Cloud Infrasturucture)で構築するマイクロサービス](https://qiita.com/takuya_0301/items/c3b45b314a9c14b1c8d6)

実はHelidonを使うとOCIサービスやリソースに楽々にアクセスできるのです！！
今回はHelidonを使うとなぜOCIを楽々操作できるかを早速見ていきましょう！！

# Helidon OCI SDK

早速ですが、HelidonにはHelidon OCI SDK(OCI Integrationの機能)が実装されています。

一体どういう機能かというと、一言で言えば、OCIサービスやリソースを動作するのに必要な認証を非常に楽に行える機能になります。
言葉だけだと分かりづらいと思うので、早速コードで見ていきましょう。

まずは、通常のOCI SDKでは各サービスを操作するためにどのような処理が必要かを見ていきます。
通常のOCI SDKにおける認証のやり方は大きく以下の3種類あります。

* APIキーによる認証
* インスタンス(リソース)プリンシパルによる認証（インスタンスプリンシパルとリソースプリンシパルは正確には別の認証方法ですが、手法としてはほとんど変わらないので、今回は同じ認証方法として扱います）
* セッショントークンベースの認証

これら3種類の認証のコードは以下のようになります。(今回はHelidonがテーマなので、Javaのサンプルコードを記載します)
データベースのクライアントの生成を例にします。

> APIキーによる認証

この方法では、事前にOCI構成ファイルを`~/.oci/config`として作成しておく必要があります。
詳細は[こちら](https://oracle-japan.github.io/ocitutorials/intermediates/using-cli/#3-1-cli%E6%A7%8B%E6%88%90%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E8%A8%AD%E5%AE%9A)です。

```java
public static void main(String[] args) throws Exception {
    String configurationFilePath = "~/.oci/config";
    String profile = "DEFAULT";

    final ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault();

    final AuthenticationDetailsProvider provider =
            new ConfigFileAuthenticationDetailsProvider(configFile);

    DatabaseClient databaseClient = new DatabaseClient(provider);
}
```

> インスタンス(リソース)プリンシパルによる認証

この方法では、事前に動的グループとポリシーを作成しておく必要があります。
詳細は[こちら](https://docs.oracle.com/ja-jp/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm)です。

```java
public static void main(String[] args) throws Exception {

    InstancePrincipalsAuthenticationDetailsProvider provider;
    provider = InstancePrincipalsAuthenticationDetailsProvider.builder().build();

    DatabaseClient databaseClient = new DatabaseClient(provider);
}
```

> セッショントークンベースの認証

この方法では、事前に[こちらの手順に従い、](https://docs.oracle.com/ja-jp/iaas/Content/API/SDKDocs/clitoken.htm)security_token_fileパラメータをOCI構成ファイル（`~/.oci/config`)に記載する必要があります。

```java
public static void main(String[] args) throws IOException {
    final AuthenticationDetailsProvider provider =
            new SessionTokenAuthenticationDetailsProvider();

    DatabaseClient databaseClient = new DatabaseClient(provider);
}
```

このように、通常SDKを利用する場合は、認証情報(上記コードの`provider`)を各サービスのクライアント（上記コードでは`client`)に渡す必要があります。

これをHelidonのOCI Integration(Helidon OCI SDK)を使うとどうなるかを見てみましょう！！

```java

```

何も書かれていませんが、間違いではありません。そうです。
Helidon OCI SDKでは認証を行うコードが不要なのです。

その代わり、CDIを利用してクライアントをインジェクションする必要があります。
インジェクションはコンストラクタとフィールドに対して行えます。
例えば、以下のようなコードになります。

> コンストラクタでのインジェクション例
```java
public class OciAtpResource {

    private final DatabaseClient databaseClient;

    @Inject
    OciAtpResource(DatabaseClient databaseClient) {
        this.databaseClient = databaseClient;
    }
}
```

>フィールドへのインジェクション例

```java
@Inject
private DatabaseClient databaseClient;
```

ここで宣言されている`databaseClient`は、認証情報を渡すことなくそのまま利用できます。
正確にはHelidon OCI SDKが裏側で自動的に認証情報を取得しています。

なので、そのまま以下のようにクライアントを利用してデータベースの動作を行うことができます。

```java
GenerateAutonomousDatabaseWalletResponse walletResponse =
                databaseClient.generateAutonomousDatabaseWallet();
```

このようにHelidonのOCI Integration(Helidon OCI SDK)を使うと簡単にOCIサービスやリソースを動作することができます。

この時、Helidon OCI SDKがどの認証手法を利用するかというのはconfigとして指定できます。
Helidonには`META-INF/microprofile-config.properties`もしくは`application.yaml`という設定ファイルを持たせることができます。
この設定ファイルの中にOCIに関する設定値を持たせることができ、認証手法は`oci.auth-strategies`として定義できます。
`oci.auth-strategies`は以下から選択でき、Stringの配列として定義することができるので、複数の定義が可能です。

* auto
* config
* config-file
* instance-principals
* resource-principal

設定値がない場合もしくは`auto`を指定した場合は、いずれかの認証手法が自動的に選択されます。
詳細は[こちら](https://helidon.io/docs/v3/apidocs/io.helidon.integrations.oci.sdk.cdi/io/helidon/integrations/oci/sdk/cdi/OciExtension.html)にあります。

ここで紹介した機能を利用するための依存関係について説明します。

まずはHelidon OCI SDKのCDIを利用するためのライブラリです。

```xml
<dependency>
    <groupId>io.helidon.integrations.oci.sdk</groupId>
    <artifactId>helidon-integrations-oci-sdk-cdi</artifactId>
    <scope>runtime</scope>
</dependency>
```

そしてOCI SDKの依存関係を追加します。
Helidon3系の場合は関連する依存関係も含めて以下のようになります。
今回はOCI SDKのバージョンが`2.37.0`です。


```xml
<dependency>
  <groupId>com.oracle.oci.sdk</groupId>
  <artifactId>oci-java-sdk-shaded-full</artifactId>
  <version>2.37.0</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.70</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.32</version>
    <scope>runtime</scope>
</dependency>
```

依存関係については以上です。

# Helidon OCI SDKを使ってATPを操作してみよう

では、最後にOKE上でHelidonからATPを動作してみます。

今回はHelidon OCI SDKのクライアントを利用して、Autonomous DatabaseのWalletファイルを生成し、クエリ結果を取得するコードを書いてみました。
コードの一部ですが、以下に記載します。
今回はクライアントに対するインジェクションはコンストラクタで実施しています。
コードの全体は後ほどレポジトリを記載します。

```java
@Path("/atp")
public class OciAtpResource {
    private static final Logger LOGGER = Logger.getLogger(OciAtpResource.class.getName());

    private final DatabaseClient databaseClient;
    private final PoolDataSource atpDataSource;
    private final String atpTnsNetServiceName;

    private final String atpOcid;
    private final String walletPassword;

    @Inject
    OciAtpResource(DatabaseClient databaseClient, @Named("atp") PoolDataSource atpDataSource,
                @ConfigProperty(name = "oracle.ucp.jdbc.PoolDataSource.atp.tnsNetServiceName") String atpTnsNetServiceName,
                @ConfigProperty(name = "oci.atp.ocid") String atpOcid,
                @ConfigProperty(name = "oci.atp.walletPassword") String walletPassword) {
        this.databaseClient = databaseClient;
        this.atpDataSource = Objects.requireNonNull(atpDataSource);
        this.atpTnsNetServiceName = atpTnsNetServiceName;
        this.atpOcid = atpOcid;
        this.walletPassword = walletPassword;
    }

    /**
     * Walletを作成してDBに接続し結果を返す
     *
     * @return Response
     */
    @GET
    @Path("/wallet")
    public Response generateWallet() {
        ResponseHelper.shouldAutoCloseResponseInputStream(false);
        GenerateAutonomousDatabaseWalletResponse walletResponse =
                databaseClient.generateAutonomousDatabaseWallet(
                        GenerateAutonomousDatabaseWalletRequest.builder()
                                .autonomousDatabaseId(this.atpOcid)
                                .generateAutonomousDatabaseWalletDetails(
                                        GenerateAutonomousDatabaseWalletDetails.builder()
                                                .password(this.walletPassword)
                                                .build())
                                .build());

        if (walletResponse.getContentLength() == 0) {
            LOGGER.log(Level.SEVERE, "GenerateAutonomousDatabaseWalletResponse is empty");
            return Response.status(Response.Status.NOT_FOUND).build();
        }

        byte[] walletContent = null;
        try {
            walletContent = walletResponse.getInputStream().readAllBytes();
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Error processing GenerateAutonomousDatabaseWalletResponse", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }
        String returnEntity = null;
        try {
            this.atpDataSource.setSSLContext(getSSLContext(walletContent));
            this.atpDataSource.setURL(getJdbcUrl(walletContent, this.atpTnsNetServiceName));
            try (
                    Connection connection = this.atpDataSource.getConnection();
                    PreparedStatement ps = connection.prepareStatement("SELECT 'Hello world!!' FROM DUAL");
                    ResultSet rs = ps.executeQuery()
            ){
                rs.next();
                returnEntity = rs.getString(1);
            }
        } catch (SQLException e) {
            LOGGER.log(Level.SEVERE, "Error setting up DataSource", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }

        return Response.status(Response.Status.OK).entity(returnEntity).build();
    }
```

この場合に利用する設定については、今回は`application.yaml`として以下のように定義しました。

```yaml
server:
  port: 8080

oracle:
  ucp:
    jdbc:
      PoolDataSource:
        atp:
          connectionFactoryClassName: oracle.jdbc.pool.OracleDataSource
          tnsNetServiceName: "${atp.db.tnsNetServiceName}"
          user: "${atp.db.user}"
          password: "${atp.db.password}"

oci:
  atp:
    ocid: "${oci.properties.atp-ocid}"
    walletPassword: "${oci.properties.atp-walletPassword}"
  auth-strategies: instance-principals
```

`oci.auth-strategies`の値から分かるように今回はインスタンスプリンシパルを利用するようにしています。
もちろん、対象のサービスやリソースに対する動的グループとポリシーの設定は必要です。

今回はOKEを利用するので、環境変数をSecretリソースとして定義します。

```sh
kubectl create secret generic atp-secret --from-literal=tnsNetServiceName=<servicename> --from-literal=password=<ユーザのpassword>  --from-literal=<ATPのOCID> --from-literal=atp-walletPassword=<Adminユーザのパスワード> --from-literal=user=<ユーザ名>
```

Manifestにも同様に定義してきます。
上記で作成したSecretを利用します。

```yaml
kind: Deployment
apiVersion: apps/v1
metadata:
  name: helidon-atp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helidon-atp
  template:
    metadata:
      labels:
        app: helidon-atp
        version: v1
    spec:
    containers:
    - name: helidon-atp
      image: iad.ocir.io/orasejapan/helidon-atp
      imagePullPolicy: Always
      ports:
        - containerPort: 8080
      env:
      - name: atp.db.tnsNetServiceName
        valueFrom:
          secretKeyRef:
            name: atp-secret
            key: tnsNetServiceName
      - name: atp.db.user
        valueFrom:
          secretKeyRef:
            name: atp-secret
            key: user
      - name: atp.db.password
        valueFrom:
          secretKeyRef:
            name: atp-secret
            key: password
      - name: oci.properties.atp-ocid
        valueFrom:
          secretKeyRef:
            name: atp-secret
            key: atp-ocid
      - name: oci.properties.atp-walletPassword
        valueFrom:
          secretKeyRef:
            name: atp-secret
            key: atp-walletPassword
```

これで環境が構築できます。

# まとめ

今回ご紹介してきた構成のサンプルアプリは以下に置いています。

https://github.com/tniita/helidon-atp/

皆さんもHelidonを使ってどんどんOCIを使っていきましょう！！

# 参考情報

https://helidon.io/docs/v3/#/mp/integrations/oci