Oracle Cloud Infrastructure (OCI) Functions with Python SDK

シリコンバレーのDeveloperWeek2023 HackathonでOCIを使ってOverall winnerとSponsor Prizeを獲得しました。
OCIの各種PaaSサービスを使ったサーバレスアーキテクチャを組んだので、まとめておきたいと思います。
https://github.com/Contact-IoT-Digital-Signage

## 今回の記事の内容
OCI Functionsから、OCIの各種PaaSサービスにアクセスする方法
- Vault
- NoSQL
- Object Storage
- Media Flow

## 環境
OCI https://cloud.oracle.com/
Python SDK https://docs.oracle.com/en-us/iaas/tools/python/2.92.0/

## (共通)サイナー取得
PythonSDKでOCIのClientを生成する場合、signerを引数に取ります。
実環境のFunctionsで実行する場合、signerを取得するのは以下の式で大丈夫です。
```python
import oci
signer = oci.auth.signers.get_resource_principals_signer()
```

## Vaultアクセス
Vaultを使うとSDKのキーなどを安全に保管できるだけではなく、コードを変更せずにキーを更新したりできるので便利です。

- secret_ocid
Secret InformationのOCIDを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/639825c0-db24-fd04-3db7-4512510f8e22.png)
- デコード
get_secret_bundleで取得したシークレットはbase64でエンコードされているので、デコード処理が必要です。
- バージョン
この処理で取得したシークレットは、最新バージョンになります。（以下画像でlatestがついてるもの）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/10a45239-09c2-783c-3be5-7794db0ada32.png)


```python
oci.secrets.SecretsClient({}, signer=signer)
secret_base64 = secret_client.get_secret_bundle(secret_ocid).data.secret_bundle_content.content.encode('utf-8')
secret = base64.b64decode(secret_base64).decode("utf-8")
```

## NoSQLアクセス
フルマネージドなキーバーリューストアが使えます。
SQLのような操作も提供されていて使いやすかったです。

### Insert
TableのOCIDを指定して、`update_row`を実行します。

```python
table_ocid = os.getenv("TABLE_OCID")
nosql_client = oci.nosql.NosqlClient({}, signer=signer)
nosql_client.update_row(
    table_name_or_id=table_ocid,
    update_row_details=oci.nosql.models.UpdateRowDetails(
        value={
            'userId': "U123456789",
            'userName': "John Doe"
        }
    )
)
```

### List
SQLライクな書き方ができるみたいです。

```python
compartment_ocid = os.getenv("COMPARTMENT_OCID")
table_name = os.getenv("TABLE_NAME")
nosql_client = oci.nosql.NosqlClient({}, signer=signer)
statement = f'select * from {table_name}'
prepare_statement_response = nosql_client.prepare_statement(compartment_id=compartment_ocid,statement=statement)

query_response = nosql_client.query(
    query_details=oci.nosql.models.QueryDetails(
        compartment_id=compartment_ocid,
        statement=prepare_statement_response.data.statement,
        is_prepared=True,
        consistency="EVENTUAL"
    )
)

return query_response.data
```

### Update
プリペアードステートメントが使えます。これもSQLの書き方ですね。
```python
compartment_ocid = os.getenv("COMPARTMENT_OCID")

nosql_client = oci.nosql.NosqlClient({}, signer=signer)
table_name = os.getenv("TABLE_NAME")
update_statement = f'update {table_name} set isActive = true where tpc = $Userid and isActive = false'

prepare_statement_response = nosql_client.prepare_statement(compartment_id=compartment_ocid,statement=update_statement)

nosql_client.query(
    query_details=oci.nosql.models.QueryDetails(
        compartment_id=compartment_ocid,
        statement=prepare_statement_response.data.statement,
        is_prepared=True,
        consistency="EVENTUAL",
        variables={
            '$Userid': 'U123456789'
        }
    )
)
```
しかしSQLライクな書き方でsetのところに変数を使ってUpdateを発行してもうまくいかなかったので、一旦GetしてInsertしなおす方法を取りました。要検証・・・。（上の例ではsetの中身はisActive = falseと固定だけど、本当には`isActive = $Isactive`みたいにしたかったんですよね。
```python
table_ocid = os.getenv("TABLE_OCID")
compartment_ocid = os.getenv("COMPARTMENT_OCID")
user_id = "U123456789"

user = nosql_client.get_row(
    table_name_or_id=table_ocid,
    key=[user_id],
    compartment_id=compartment_ocid
)

nosql_client.update_row(
    table_name_or_id=table_ocid,
    update_row_details=oci.nosql.models.UpdateRowDetails(
        value={
            'userId': user_id,
            'userName': user.data.value['userName'],
            'age': '20'
        }
    )
)
```

### Delete
キー指定して消すだけです。
```python
table_ocid = os.getenv("TABLE_OCID")
compartment_ocid = os.getenv("COMPARTMENT_OCID")

nosql_client = oci.nosql.NosqlClient({}, signer=signer)

nosql_client.delete_row(
    table_name_or_id=table_ocid,
    key=["U123456789"],
    compartment_id=compartment_ocid
)   
```

## Object Storage
S3互換のオブジェクトストレージです。動画などのメディアファイルからJsonファイルの格納まで、いろいろなことができます。
Functionsには実行環境の中にテンポラリー領域があるので、ファイルを一旦Functionsのメモリに落とすことも可能です。

### アップロード
```python
BUCKET_NAME_SPACE = os.getenv("BUCKET_NAME_SPACE")
BUCKET_NAME = os.getenv("BUCKET_NAME")

client = oci.object_storage.ObjectStorageClient(config={}, signer=signer)
with open(tmp_file_path, "rb") as f:
    data = f.read()
    client.put_object(BUCKET_NAME_SPACE, BUCKET_NAME, "sample.mp4", data)
```

### ダウンロード
JSONファイルをダウンロードしてオブジェクトとして扱うのを例にします。
```python
object_name = "ダウンロードしたいオブジェクトの名前"
file_content = client.get_object(BUCKET_NAME_SPACE, BUCKET_NAME, object_name).data.content
json_data = json.loads(source_file)
```

## Media Flow
ジョブ実行です。フロー定義はあらかじめGUIからやっておいて、ジョブ実行のみをスクリプトで行いましたが、ちょっと曲者でした。
- media_workflow_idを設定するためには、`CreateMediaWorkflowJobDetails`ではなく`CreateMediaWorkflowJobByIdDetails`を使うこと
- workflow_identifier_typeはmedia_workflow_idとは違うということ
- パラメーターの上書きはjob実行時のjsonファイルをOCIのGUIから確認しながら設定するとよい(View JSONのところからJSONが見れるので、そこのParametersを確認）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/717f5c41-c1c6-e56e-a117-d9c74c11fab5.png)


```python
media_flow = oci.media_services.MediaServicesClient({}, signer=signer)

compartment_id = os.environ.get('COMPARTMENT_ID')
media_workflow_id = os.environ.get('MEDIA_WORKFLOW_ID')

job_details = oci.media_services.models.CreateMediaWorkflowJobByIdDetails(
    compartment_id=compartment_id,
    workflow_identifier_type=oci.media_services.models.CreateMediaWorkflowJobDetails.WORKFLOW_IDENTIFIER_TYPE_ID,
    media_workflow_id=media_workflow_id,
    display_name=object_file_name,
    parameters={
        "input": {
            "objectName": object_name, # 実行時のパラメータをここで指定
            "objectNameFilename": object_file_name # 実行時のパラメータをここで指定
        },
    }
)
response = media_flow.create_media_workflow_job(job_details)
```

## まとめ
OCI Functionsを中心にOCIの各種サービスにPython SDKでアクセスする方法をまとめました。
基本的にはPython SDKのドキュメントと戦うことになると思います。
今回OCIを利用するのは初めてでしたが、サンプルコードが少なかったので苦労した点が多く、それを解決すべくこの記事を書きました。

質問、ご指摘歓迎します。