[Cloud] Microsoft AzureとOracle Cloud Infrastructure上でマルチクラウドアプリケーションを構築/Building Multicloud Apps on Microsoft Azure and Oracle Cloud Infrastructure

原文はこちら

マルチクラウドというストーリーが拡がっており、エンタープライズ企業は複数のインフラ、そしてクラウドプロバイダにまたがってアプリケーションを構築することを望んでいます。最近発表されたOracleとMicrosoftのクラウド・パートナーシップにより、Microsoft AzureとOracle Cloud Infrastructure(OCI)間でのクロス・クラウド・ネットワークや、統一されたIDおよび認証、そして協働的で統合されたサポートモデルが可能になっています。クロス・クラウド・ネットワークはセキュアかつ専有されたリンクであり、ふたつのクラウドプロバイダ間の通信に大容量の帯域と低遅延性を提供します。お客様はこのクロス・クラウドの接続を構築するのに、サードパーティのネットワークプロバイダに依頼する必要がありません。
クロス・クラウド接続によって以下を含む多くのユースケースが可能になります:
  • WebアプリケーションをAzure上に、Autonomous DatabaseをOCI上に
  • JavaとOracleアプリケーションをOCI上に、.NET/SQLサーバーアプリケーションをAzure上に
  • AzureからOCI上のAutonomous Databaseのデータマイニング
  • クラウドをまたいだアプリケーション間のインターオペラビリティ
  • 複数クラウドベンダをまたいだ高可用性
Jamal Arifは最近のこのブログポストで、ユーザーがMicrosoft AzureとOCIとの間のエンドトゥエンドのクラウド相互接続をどれほどかんたんに作成できるかを説明しました。そのシリーズの続きとして、このポストではわたしたち(Samir ShahVinay Rao)からマルチクラウド・2層アプリケーションの構築プロセスを一通りご説明することで、エンタープライズ企業がふたつのクラウドプロバイダそれぞれのベストなサービスを活用したアプリケーションをどのように構築できるかのイメージをつかんでいただこうと思います。
アプリケーションの構築を始めるまえに、アーキテクチャの概要を見ておきましょう。以下の図で、アプリケーション層はAzureコンピュートインスタンス上に居て、データベース層はOracle Autonomous Databaseサービスとなっています。アプリケーション層はデータベースにデータを書き込み、またクエリしますが、この際にトランジット・ルーティングを用いることでセキュアなプライベート接続を通ってアクセスします。

前提の準備事項

アプリケーションのデプロイを始めるまえに、How to set up the interconnect between Oracle Cloud Infrastructure and Microsoft Azureのポストで説明されているステップをこなしておきましょう。

Step 1: OCI上でAutonomous Databaseをセットアップ

  1. Oracle Cloud Infrastructureコンソールにログインしてus-ashburn-1リージョンを選んでください。
  2. クイックアクションメニューからCreate a data warehouseをクリックしてAutonomous Data Warehouseインスタンスをつくります。
  3. ウィザードに従ってAutonomous Data Warehouseインスタンスに必要な項目を入力し、Create Autonomous Databaseをクリック。
    データベースのプロビジョニングには3~5分程度かかります。
  4. Walletファイル(クライアント認証情報)をダウンロードします。
    1. Autonomous Databaseのページから、データベースの横のActionsのアイコン(「・・・」アイコン)をクリックしてService Consoleを選んでください。
    2. Administrationのセクションに移動してWalletファイルをダウンロードします。
      のちのステップでこのWalletファイルを使うことになります。

Step 2: Azure上でのアプリケーション層のセットアップ

  1. Azureポータルにログインします。
  2. 前提の準備事項をこなしているときに作成したネットワークの中にUbuntuのVirtual Machine(VM)を作成します。
  3. ネットワークセキュリティグループの設定で、必要となるデータベースのポートおよび他の接続の許可を行います。
  4. VMが起動したら、SSHでVMに接続してみてアクセス可能なことを確かめましょう。

Step 3: Oracle Servicesへのプライベートピアリングルートをセットアップ

前提の準備事項のステップをこなしていれば、OCIとMicrosoft Azureとの間のプライベートピアリングリンクができあがっていると思います。であれば、ふたつのインフラストラクチャ上のVMはお互いに対してセキュアに接続することができます。
データベースにセキュアプライベート接続を通してアクセスするには、最近アナウンスされたトランジット・ルーティング の機能を使います。この機能を有効にするには、Access Oracle Services Privately with a Service Gatewayのブログポストでの手順に従ってサービスゲートウェイを作成します。
  1. OCIコンソールで、Networking > Virtual Cloud Networkと移動し、VCNを選択してください。
  2. Resources配下のService Gatewaysをクリックし、サービスゲートウェイを作成します。
  3. All IAD Services in Oracle Services Networkに到達できるサービスゲートウェイを作成します。
    サービスゲートウェイが作成できたら、ふたつのルートテーブルを作成します。ひとつはDRGルートテーブル、もうひとつはSGWルートテーブルです。
  4. DRGルートテーブルの作成
    1. Resourses配下のRoute Tablesをクリックし、新規ルートテーブルを作成します。
    2. ターゲットタイプとしてService Gatewayを指定し、デスティネーションサービスはAll IAD Services in Oracle Services Networkを指定します。
  5. このルートテーブルをDRGに関連付けることで、全てのOracle Services NetworkルートをこのDRGの監督化に置きます。
    1. Resources配下でDynamic Routing Gatewaysクリックします。
    2. 作成したルートテーブルがあるVCNに付属のDRGをクリックします。
    3. Actionsアイコン(「・・・」のアイコン)をクリックして、そこからAssociate With Route Tableをクリックします。
    4. 作成したルートテーブルを選択し、Associateをクリックします。
    これでルートテーブルがDRGに関連付けられたので、DRGがAzure Expressルート上のOracle Services Networkルートを監督するようになり、また、これらのルートはVMのルートテーブル上で見えるようになります。
  6. Oracle Services NetworkからのトラフィックがリモートのAzureアプリケーションに到達できるようにするため、SGWルートテーブルを作成します。ターゲットタイプとしてDynamic Routing Gatewayを指定し、デスティネーションCIDRブロックにはAzure Expressルートのアドレスを指定します。
  7. SGWルートテーブルをサービスゲートウェイに関連付けます。
    1. Resources配下のService Gatewayをクリック。
    2. 対象のサービスゲートウェイの横のActionsアイコン(「・・・」のアイコン)をクリックして、Associate With Route Tableをクリック.
    3. 作成したルートテーブルを選択し、Associateをクリック。

Step 4: アプリケーションのインストール

これで接続のセットアップができたので、データベースからデータをマイニングするアプリケーションをインストールします。
  1. Step 1でダウンロードしておいたWalletファイルをアップロードし、フォルダにZIPを解凍し、その中のファイルを使ってAutonomous Data Warehouseデータベースにアクセスします。
  2. このリポジトリからアプリケーションをクローンします。
  3. アプリケーションを実行します。
これで完了です!このシンプルなPythonアプリケーションがAzure VNET上で稼働し、Autonomous Data Warehouseデータベースからプライベートかつセキュアにデータをマイニングできます。

まとめ

このポストをお楽しみいただけたら幸いです。AzureとOracle Cloud Infrastructureをまたいでお客様がどんなアイデアやアプリケーションを実現することができるかの、必要最小限の例として、ここではサンプルの2層アプリケーションをご紹介しました。

[Functions] Key Managementを使った設定変数の暗号化と復号/Using Key Management To Encrypt And Decrypt Configuration Variables

原文はこちら

最近このブログではOracle Functionsに関連する様々なテーマについてご紹介してきましたが、今回ご紹介するのはもしかするとこのブログシリーズで最初に取り上げるべきだったかもしれないテーマです。以前のポストでアプリケーションとファンクションの設定変数の設定方法をご説明したんですが、こうした設定変数をセキュアに保持しておく方法はそのときにはご説明していませんでした。このポストでは、Oracle CloudテナンシーのKey Managementを使って設定を暗号化、復号することによってこのセキュアな保持を実現する方法をご説明します。

これをやるにはいくつかのステップを進んでいくことになるので、ここでアウトラインを示しておきますね:
  • KMS Vaultの作成
  • Master Encryption Keyの作成
  • Data Encryption Key (DEK) をMaster Encryption Keyから生成
  • DEK plaintext から返却された値を使って sensitive value を暗号化(オフライン)
  • 暗号化された sensitive value をサーバレスアプリケーションの設定変数として保存
  •  sensitive value の暗号化に使ったDEK ciphertext と initVector をファンクションの設定変数として保存
  • ファンクションの中でOCIDとCryptographic Endpointを使ってOCI KMS SDKを呼び出し、DEK ciphertext を復号して plaintext に戻す
  • 復号したDEK plaintext と initVectorを使ってsensitive value を復号する
ここで sensitive value と記載しているものは、暗号化が必要なものであればなんでも有り得ます。データベースパスワードだったり、APIキーだったり、などなど。Oracle Functionsの設定変数の中に平文ではなく暗号化して保存したいものを示すプレースホルダーであり、実態がなんであれ構いません。
なんかステップ多いな、って感じですよね。でも実のところ書いてあるとおり進めていけばそんなに大変じゃありません。それに、本当のところ、アプリケーションとファンクションにとってセキュリティというのは一番大事なことですからね。

始める前に

ここではOracle Functionsの中でリソースプリンシパルを活用することになります。これはすなわち、OCI SDKを使う際にOCI設定ファイルを含める必要は生じないということですが、一方で、KMS APIとやり取りするうえでの適切な許可ポリシーを備えたダイナミックグループを作成しておく必要があるということになります。
まず、'Dynamic Group'を作成して'rules'を以下のようにセットしてください(ファンクションをデプロイするコンパートメントのOCIDを使ってください):
ダイナミックグループとリソースプリンシパルをOracle Functionsと組み合わせて使う方法について詳しく知りたければドキュメントを参照ください。次のステップはダイナミックグループ用にポリシーを作り、 keysvaults と key-delegate の管理権限を与えることです。こちらについても、ポリシーのドキュメントを読んでどのようなポリシーを適用としているのか理解してください。

アプリケーションとファンクションの作成

では、サーバレスアプリケーションを作成しましょう(適切なサブネットOCISで置き換えてください):
fn create app --annotation oracle.com/oci/subnetIds='["ocid1.subnet.oc1.phx..."]' fn-kms
そしてファンクションを作成します:
fn init --runtime java fn-kms-demo

依存対象

KMS SDKのための依存関係を pom.xml に追加しておく必要があります:
<dependency>
    <groupId>com.oracle.oci.sdk</groupId>
    <artifactId>oci-java-sdk-keymanagement</artifactId>
    <version>1.6.0</version>
</dependency>
view rawpom.xml hosted with ❤ by GitHub
Java 11をお使いの場合は、 javax.activation-apiもマニュアルで追記しておきます:
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>javax.activation-api</artifactId>
    <version>1.2.0</version>
</dependency>
view rawpom.xml hosted with ❤ by GitHub

Dockerfile

いくつかの環境変数に依存することになるので、お手製の Dockerfile を作成する必要があります。デフォルトで実行される Dockerfile との違いはファンクションをデプロイする際のDockerビルドでのテスト実行をスキップするところだけです。なぜこれをスキップするかというと、現状ではでファンクションをデプロイする際に環境変数をDockerビルドコンテキストに渡す術がないからです。以下のお手本を使ってください:
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 ["codes.recursive.KmsDemoFunction::handleRequest"]
view rawDockerfile hosted with ❤ by GitHub
ファンクションをデプロイする前にマニュアルでテストを実行することをお忘れなく!

VaultとMaster Encryption Keyの作成

OCIコンソールのサイドバーから、'Governance and Administration'の配下の'Security'→'Key Management'を選択してください:
'Create Vault'をクリックしてVaultの詳細を入力します:
Vaultが作成できたら、Vaultの名前をクリックするとVaultの詳細が表示されます。詳細画面で'Create Key'をクリックして新しいMaster Encryption Keyを作成し、ダイアログの入力欄を埋めていきます:
Master Encryption KeyのOCIDとVaultの'Cryptographic Endpoint'をコピーしておいてください。これはあとでDBパスワード用のData Encrption Key(DEK)を作成する際に使います。

Data Encryption Key (DEK)の作成

以下のようにData Encrption Key(DEK)をOCI CLIから作成します:
oci kms crypto generate-data-encryption-key \
--key-id ocid1.key.oc1.phx.... \
--include-plaintext-key true \
--key-shape "{\"algorithm\": \"AES\", \"length\": 16}" \
--endpoint [Cryptographic Endpoint]
view rawgenerate-dek.sh hosted with ❤ by GitHub
 generate-data-encryption-key から返ってくる ciphertext と plaintext の値を手元に持っておいてください。あとですぐに必要になります。
DEK ciphertextの例
I...[random chars]...​AAAAAA==
ciphertextをアプリケーションの設定変数として保持させます:
fn config app fn-kms DEK_CIPHERTEXT I...[random chars]...​AAAAAA==
DEK plaintextの例
0…​[random chars]...=

パスワードの暗号化

このステップではパスワードをファンクションの中ではなく、オフラインで暗号化します。あとでファンクションは稼働時に復号を行うことになります。スタンドアロンのJavaプログラムの中で先程のDEKを使ってパスワードを暗号化します。以下のお手本を使ってもらってもよいです。
注意:Plug in your DEK plaintext の値をプラグインして initVectorにランダムな16byteのStringを与えてください。 initVector はあとで復号の際に使えるように設定変数として保持します。
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
class Main {
    private static String key = "0...=="; //DEK plaintext value
    private static String initVector = "abcdefghijklmnop"; //must be 16 bytes
    public static void main(String[] args) {
        System.out.println(encrypt("hunter2"));
    }
    public static String encrypt(String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            return Base64.getEncoder().encodeToString(encrypted);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}
view rawEncrypt.java hosted with ❤ by GitHub
ランダムな16 byteの initVector Stringをアプリケーションの設定変数として保持させます:
fn config app fn-kms INIT_VECTOR_STRING [Random 16 byte string]
上述のプログラムの出力した値をコピーしてください。これが暗号化されたパスワードです。これもアプリケーションの設定変数として保持させます:
fn config app fn-kms ENCRYPTED_PASSWORD N...==
最後に、Master Encryption KeyのOCIDとCryptographic Endpointをアプリケーションの設定変数として格納します:
fn config app fn-kms KEY_OCID ocid1.key.oc1.phx...
fn config app fn-kms ENDPOINT https://...-crypto.kms.us-phoenix-1.oraclecloud.com

サーバレスファンクション

これでサーバレスファンクションに暗号化されたパスワードを復号するよう修正を加えることができるようになりました。以下のようになります:
package codes.recursive;
import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider;
import com.oracle.bmc.keymanagement.KmsCryptoClient;
import com.oracle.bmc.keymanagement.model.DecryptDataDetails;
import com.oracle.bmc.keymanagement.requests.DecryptRequest;
import com.oracle.bmc.keymanagement.responses.DecryptResponse;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.util.Base64;
import java.util.Map;
public class KmsDemoFunction {
    private final String initVector;
    public KmsDemoFunction() {
        this.initVector = System.getenv().get("INIT_VECTOR_STRING");
    }
    public Map<String, String> decryptSensitiveValue() throws IOException {
        Boolean useResourcePrincipal = Boolean.valueOf(System.getenv().getOrDefault("USE_RESOURCE_PRINCIPAL", "true"));
        String encryptedPassword = System.getenv().get("ENCRYPTED_PASSWORD");
        String cipherTextDEK = System.getenv().get("DEK_CIPHERTEXT");
        String endpoint = System.getenv().get("ENDPOINT");
        String keyOcid = System.getenv().get("KEY_OCID");
        /*
        * when deployed, we can use a ResourcePrincipalAuthenticationDetailsProvider
        * for our the auth provider.
        * locally, we'll use a ConfigFileAuthenticationDetailsProvider
        */
        AbstractAuthenticationDetailsProvider provider = null;
        if( useResourcePrincipal ) {
            provider = ResourcePrincipalAuthenticationDetailsProvider.builder().build();
        }
        else {
            provider = new ConfigFileAuthenticationDetailsProvider("/.oci/config", "DEFAULT");
        }
        KmsCryptoClient cryptoClient = KmsCryptoClient.builder().endpoint(endpoint).build(provider);
        DecryptDataDetails decryptDataDetails = DecryptDataDetails.builder().keyId(keyOcid).ciphertext(cipherTextDEK).build();
        DecryptRequest decryptRequest = DecryptRequest.builder().decryptDataDetails(decryptDataDetails).build();
        DecryptResponse decryptResponse = cryptoClient.decrypt(decryptRequest);
        String decryptedDEK = decryptResponse.getDecryptedData().getPlaintext();
        String decryptedPassword = decrypt(encryptedPassword, decryptedDEK);
        /*
        * returning the decrypted password for demo
        * purposes only. in your production function,
        * obviously you should not do this.
        */
        return Map.of(
                "decryptedPassword",
                decryptedPassword
        );
    }
    private String decrypt(String encrypted, String key) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encrypted));
            return new String(original);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}
view rawKmsDemoFunction.java hosted with ❤ by GitHub

テスト

前述のように、ファンクションのテストをマニュアルで行う必要があります。
ローカルでファンクションをテストできるようにするには、いくつかの環境変数をセットしておく必要があります。GitHubプロジェクトのルートにある env.sh を見るとどの変数をセットする必要があるかわかります(あるいは↓からコピーしてください)。これらの値は全て上述のステップから取得できます(格納しておいたアプリケーションの設定変数と対応していることに留意ください)。
#!/usr/bin/env bash
export ENCRYPTED_PASSWORD=
export INIT_VECTOR_STRING=
export KEY_OCID=
export DEK_CIPHERTEXT=
export ENDPOINT=
export USE_RESOURCE_PRINCIPAL=false
view rawenv.sh hosted with ❤ by GitHub
必要な環境変数をセットしたら、ユニットテストを書きましょう:
package codes.recursive;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fnproject.fn.testing.*;
import org.junit.*;
import java.io.IOException;
import java.util.Map;
import static org.junit.Assert.*;
public class KmsDemoFunctionTest {
    @Rule
    public final FnTestingRule testing = FnTestingRule.createDefault();
    @Test
    public void shouldDecryptPassword() throws IOException {
        testing.givenEvent().enqueue();
        testing.thenRun(KmsDemoFunction.class, "decryptSensitiveValue");
        FnResult result = testing.getOnlyResult();
        System.out.println(result.getBodyAsString());
        Map<String, String> resultMap = new ObjectMapper().readValue(result.getBodyAsString(), Map.class);
        assertEquals("hunter2", resultMap.get("decryptedPassword"));
    }
}

デプロイ

以下でファンクションをデプロイします:
fn deploy --app fn-kms
呼び出しは以下:
fn invoke fn-kms fn-kms-demo
復号されたパスワードが返却されるでしょう:
{"decryptedPassword":"hunter2"}

まとめ

このポストではOCI KMSを使ってVaultとMaster Encryption Keyを作成しました。そしてそのMaster Encryption Keyを使ってData Encrption Key(DEK)を作成し、DEKを使って機密情報を暗号化し、その暗号化された機密情報をサーバレスファンクションの設定変数に保存しました。そしてこの暗号化された機密情報にデプロイしたファンクションからアクセスし、ファンクションで使えるように復号しました。

参考情報

Oracle Functions関連のわたしの他のポストもチェックしてみてください:
ご参考までに、デモの中で使ったコードはGitHubで利用可能です。https://github.com/recursivecodes/fn-kms-demo