ピロローグ

主に技術ネタについて書いてみる

ApplicationGatewayとAzure Front Doorを組み合わせてみる

タイトルを見る限りだとぱっと見普通だなと思われそうですが、今回執筆するイメージは以下の通りです。

構成イメージ
構成イメージ

Application Gatewayをエンドポイントとして、そのバックエンドにFrontDoor, App Serviceと続くのですが、

  • なんだこの構成...
  • 悪手じゃないか...
  • コストが無駄にかかるだけじゃん...

と思う方もいらっしゃるかと思います。
ただ、世の中にはこういう要件を必要とされる方もいらっしゃるかもしれません。

  • 特定のIPアドレスから来たトラフィックは別のApp ServiceやVMにルーティングさせたい
  • 稼働しているサービスの要件上、どうしてもIPv4でクライアントIPを受け付ける必要がある

それまではFrontDoorをエンドポイントとして運用し、App Serviceをバックエンドとする構成が最近のスタンダードかなと思います。
急遽上記の要件を満たすためにApplicationGatewayを導入した際のポイントをまとめたいと思います。

FrontDoorの設定

特殊な設定は不要です。 Application Gatewayからのトラフィックを受信できるよう、カスタムドメインを設定しておけば良いです。

learn.microsoft.com

1つ留意事項としては、FrontDoorにて受け付けるトラフィックをApplication Gatewayに制限することは難しそうです。
Application Gateway自体の送信元IPアドレスが変更される可能性があるためです。
また、FrontDoorにWAFを追加し、カスタムルールでIP制限をかけようと試みましたが、ダメでした。

Application Gatewayの設定

こちらも特殊な設定は不要です。 カスタムドメインを設定しつつ、フロントエンドIP、リスナー、ルール、HTTP設定、バックエンドプール、カスタムプローブの各コンポーネントを設定していけば問題ないです。

ポータルから設定する learn.microsoft.com コンポーネントについて

learn.microsoft.com

個人的に留意したのは以下の通りです。

  • バックエンドプールに設定する際はFQDNをFrontDoorで設定したカスタムドメインを指定する
  • バックエンド設定の追加にて ホスト名をオーバーライドする の選択で バックエンドターゲットからホスト名を選択する を選択
  • 一通り設定した後バックエンド正常性を確認すると異常のステータスとなるが、これはサポートに問い合わせたところ現状はこのままで良いとのこと。FrontDoorとはIPv6ベースでチェックしているようだが、portal上ではNG扱いになるとのことだとか。

そのほか

前段にApplication Gatewayにすることで構成が少し複雑になりました。
App Serviceでクライアントが要求したhostを取得する場合は x-original-host を取得しましょう。
意識しないとFrontDoorのヘッダーが取得され意図しない挙動が発生する可能性があるので要注意です。

learn.microsoft.com

Azure Storage Blob SDK for Go を使ってBlobの操作を行う(接続文字列編)

最近Goに触り始めたので、アウトプットとして残してみます。 BlobuStorageから払い出される接続文字列を利用してアップロード、コンテナ内のBlob一覧表示、削除をぞれぞれサンプルコードとして残してみます。 エラーハンドリングなどは適宜追加してみてください。

前提

Go 1.17以上
Azure Storage Blob SDK for Goをインストールする

SDKインストール

クイックスタート参考にそのままインストール

go get -u github.com/Azure/azure-sdk-for-go/sdk/storage/azblob

learn.microsoft.com

サンプル

Upload, Delete, Listの全文コードは下記の通りです。 エラーハンドリングは要件に応じて書き換えてください。

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
)

func Upload(ctx context.Context, cc *azblob.ContainerClient, containerName string, blobName string, fileName string) bool {
    blobClient, err := cc.NewBlockBlobClient(blobName)
    if err != nil {
        fmt.Print(err)
        return false
    }

    data, err := os.Open(fileName)
    defer data.Close()
    if err != nil {
        fmt.Print(err)
        return false
    }

    opts := azblob.UploadOption{}
    _, err = blobClient.UploadFile(ctx, data, opts)
    if err != nil {
        fmt.Print(err)
        return false
    }

    return true
}

func Delete(ctx context.Context, cc *azblob.ContainerClient, containerName string, blobName string) bool {
    blockBlob, err := cc.NewBlockBlobClient(blobName)
    if err != nil {
        return false
    }

    _, err = blockBlob.Delete(ctx, nil)
    if err != nil {
        fmt.Print(err)
        return false
    }
    return true
}

func GetBlobList(ctx context.Context, cc *azblob.ContainerClient, containerName string, prefix string) []string {
    pager := cc.ListBlobsFlat(&azblob.ContainerListBlobsFlatOptions{
        Prefix: &prefix,
    })

    blobs := []string{}
    for pager.NextPage(ctx) {
        res := pager.PageResponse()

        for _, i := range res.Segment.BlobItems {
            blobs = append(blobs, *i.Name)
        }
    }

    return blobs
}

func main() {
    conn := <Connection String>
    ctx := context.Background()
    containerName := <container>

    serviceClient, err := azblob.NewServiceClientFromConnectionString(conn, nil)
    if err != nil {
        fmt.Print(err)
    }

    cc, err := serviceClient.NewContainerClient(containerName)
    if err != nil {
        fmt.Print(err)
    }

    fileName := <Source file Path> //絶対パスでやってみたところ動きました
    blobName := <Destination file Name>
    // upload
    result := Upload(ctx, cc, containerName, blobName, fileName)
    fmt.Print(result)

    // list
    prefix := <prefix>
    list := GetBlobList(ctx, cc, containerName, prefix)
    fmt.Print(list)

    // delete
    for _, file := range list {
        result := Delete(ctx, cc, containerName, file)
        fmt.Print(result)
    }
}

List

一覧取得。取得したいファイルのprefixをオプションとして引き渡してリクエストする。 ループして1つ1つファイル名を取得。

func GetBlobList(ctx context.Context, cc *azblob.ContainerClient, containerName string, prefix string) []string {
    pager := cc.ListBlobsFlat(&azblob.ContainerListBlobsFlatOptions{
        Prefix: &prefix,
    })

    blobs := []string{}
    for pager.NextPage(ctx) {
        res := pager.PageResponse()

        for _, i := range res.Segment.BlobItems {
            blobs = append(blobs, *i.Name)
        }
    }

    return blobs
}

Upload

アップロードしたいファイルをopenし、BlobClientからUploadします。

func Upload(ctx context.Context, cc *azblob.ContainerClient, containerName string, blobName string, fileName string) bool {
    blobClient, err := cc.NewBlockBlobClient(blobName)
    if err != nil {
        fmt.Print(err)
        return false
    }

    data, err := os.Open(fileName)
    defer data.Close()
    if err != nil {
        fmt.Print(err)
        return false
    }

    opts := azblob.UploadOption{}
    _, err = blobClient.UploadFile(ctx, data, opts)
    if err != nil {
        fmt.Print(err)
        return false
    }

    return true
}

Delete

そんなに難しいことはしてない。 削除したいBlob名を引数にclient生成してDeleteするだけ。

func Delete(ctx context.Context, cc *azblob.ContainerClient, containerName string, blobName string) bool {
    blockBlob, err := cc.NewBlockBlobClient(blobName)
    if err != nil {
        return false
    }

    _, err = blockBlob.Delete(ctx, nil)
    if err != nil {
        fmt.Print(err)
        return false
    }
    return true
}

終わりに

今回は接続文字列を利用して操作を行いましたが、SASトークンを利用した操作も後日残してみたいと思います。

リファレンス

Azure クイック スタート - Go を使用してオブジェクト ストレージに BLOB を作成する | Microsoft Learn

Azure ADをIdPにしたSAMLのSPを構築してみた

SAMLを利用したSSO認証の検証をやってみたので残しておきます。
SAMLのSPを作るために、 one-login/php-saml のdemoサイトを利用しました。
PHPでは SimpleSAMLphp での構築方法がいくつかネットに載っておりますが、 php-saml での情報がほぼなかったのでチョイスしました。

前提条件

  • Azure ADを用意すること
    AzureADへのユーザ登録も忘れずに。
  • Webサーバを構築しておくこと
    今回は nginx & php-fpm環境としてます。
    Docker & ngrokで一時的に公開しやってみたのですが、うまくできなかったので、仮想サーバ上で立てました。
  • PHP実行環境 今回は7.4で試してます。
  • one-login/php-samlをダウンロードする

手順

php-samlをダウンロードする

Webサーバの公開ディレクトリ配下にダウンロードします。
今回はgit clone しました。

SPの設定を行う

demo1/settings_example.php をコピーしdemo1/settings.phpとして作成し以下の通り登録します。
idpの配下の要素は一旦空のままで。後程Azure ADで取得した値で埋めます。

<?php
$spBaseUrl = 'https://hoge.com/php-saml';  //任意のドメインに
$settingsInfo = array (
    'sp' => array (
        'entityId' => $spBaseUrl.'/demo1/metadata.php',
        'assertionConsumerService' => array (
            'url' => $spBaseUrl.'/demo1/index.php?acs',
        ),
        'singleLogoutService' => array (
            'url' => $spBaseUrl.'/demo1/index.php?sls',
        ),
        'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
    ),
    'idp' => array (
        'entityId' => '',
        'singleSignOnService' => array (
            'url' => '',
        ),
        'singleLogoutService' => array (
            'url' => '',
        ),
        'x509cert' => '', 
    ),
);

Azure ADにカスタムアプリケーションを登録する

  • Azure Portalから Azure Active Direcotry を検索し、エンタープライズアプリケーションの画面に遷移します。
    エンタープライズアプリケーション
    Portal画面
  • +独自のアプリケーションの作成 から任意のアプリケーションを登録します。
    独自のアプリケーションの作成
    独自のアプリケーションの作成

SAML構成を行う

  • 作成が完了するとエンタープライズアプリケーションのページに遷移するので、 シングルサインオン から SAML のペインをクリックします。

    demo
    demo

  • エンティティIDと応答URLをそれぞれ登録します。

    • エンティティID https://hoge.com/php-saml/demo1/metadata.php
    • 応答URL https://hoge.com/php-saml/demo1/index.php/?acs
      基本的なSAML構成
      基本的なSAML構成
  • 登録後、SAML名証明書から フェデレーション メタデータ XML をダウンロードします。 ダウンロード完了したら、エディタで開き、<X509Certificate> タグで囲まれた文字列をコピーし、前述の SPの設定を行う で作成した settings.phpx509cert の欄にペーストします。

  • 完了後、 Azure Potalに戻り demoのセットアップ に表示されている文字列を settings.php にコピペします。

    demoのセットアップ
    demoのセットアップ

    • ログイン URL singleLogoutServiceurl
    • Azure AD 識別子 entityIdurl
    • ログアウト URL singleLogoutServiceurl

ユーザ登録を行う

このままでは誰一人、デモサイトを利用できるユーザがいないので、ユーザ登録を行います。 エンタープライズ アプリケーションのページから ユーザとグループ ブレードを開き、 +ユーザまたははグループの追加 をクリックし、Azure ADに既に登録されいているユーザの中から利用させたいユーザを登録します。

ユーザ登録
ユーザ登録

動作確認

デモサイトにアクセスし、Loginをクリックします。

Demoサイト
Demoサイト

Microsoft Login画面が開くので、ここでメールアドレスとパスワード、必要に応じて二要素認証対応を行います。

認証画面

認証が無事に完了すると、以下の通り、SAMLレスポンスで受け取った値を一覧で確認できます。

認証後ページ

おわりに

構築するのに非常に苦労したのですが、いざ出来上がるとそんなに複雑ではなかったです。
これを利用すると例えば自社内で利用する独自のアプリケーション(wikiだったり、ポータルサイトなど)をAzure ADと連携させて認証させることで堅牢なシステムを作ることができそうですね。

特定のURLのみBasic認証を無効にする

テスト環境や、特定のユーザのみにサイトを公開したいときにBasic認証等でアクセス制限を掛けるケースがあると思いますが、一部のURLのみBasic認証を無効にしたいとの要望がありましたのでその時の設定方を記録しておきます。

Satisfy Any

SetEnvIf Request_URI "^/hoge/*" ok_dir
SetEnvIf Request_URI "^/index.php" ok_dir

Order Deny,Allow
Deny from all
Allow from env=ok_dir

AuthType Basic
AuthName "Input your ID and Password."
AuthUserFile /pass/to/.htpasswd
require valid-user

リクエスURIとして環境変数を設定。環境変数にはBasic認証の無効としてURIのパスとLaravelのエントリポイントをセットする。

Azure CLIを使って仮想アプリケーションとディレクトリを変更する

チュートリアル:Azure で PHP と MySQL アプリを構築する | 仮想アプリケーション パスを設定する を参考に仮想ディレクトリを変更しようとしたらつまずいた。 どうやらURLが正しくないみたい。

az resource update --name yourproject --resource-group yourprojectrg --namespace Microsoft.Web --resource-type config --parent sites/<app_name> --set properties.virtualApplications[0].physicalPath="site\wwwroot\public" --api-version 2015-06-01
Operation failed with status: 'Not Found'. Details: 404 Client Error: Not Found for url: https://management.azure.com/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourcegroups/yourprojectrg/providers/Microsoft.Web/sites/yourproject/config/yourproject?api-version=2015-06-01

Resource Explorerから該当のリクエストURLを確認したところリソースIDの形式がマッチしてない模様。

https://management.azure.com/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/yourprojectrg/providers/Microsoft.Web/sites/yourproject/config/web?api-version=2018-02-01

力技ですが、az resource updateコマンドでidsオプションでリソースIDを指定することで対応した。 ※合わせてapi versionの日付も修正

subscription_id=$(az account show --query id)
az resource update --ids "/subscriptions/${subscription_id}/resourceGroups/yourprojectrg/providers/Microsoft.Web/sites/yourproject/config/web" --set properties.virtualApplications[0].physicalPath="site\wwwroot\public" --api-version 2018-02-01

期待通りになっている。

f:id:pir0:20190131205611p:plain
仮想アプリケーションとディレクト

上記を使えば追加もできそう。

VNet Integrationを利用してWeb apps とAzure Databaseを接続してみる

現在Web apps - Azure Database間はAzureのデータセンターネットワークを通じ接続する仕様となっているかと思いますが、先日プレビューで公開された New App Service VNet Integration feature を利用してVNet、サービスエンドポイント経由で接続できるかやってみました。
イメージ的には以下の通りです。

f:id:pir0:20190129102947p:plain
イメージ

準備

まずは各リソースを用意します。

f:id:pir0:20190128125601p:plain
リソース一覧
Web apps(App Service)はWin版をS1で設定。Azure DatabaseはMySQL版を汎用プランで設定しました。

VNetはサブネットをWeb appsのみ用意します。 Azure Databaseとサービスエンドポイント経由で接続するため、サービスの設定をお忘れなく。

f:id:pir0:20190128152405p:plain
サブネット詳細

VNet統合設定

VNetの追加(プレビュー)からVNet, サブネットをそれぞれ選択し、設定します。

f:id:pir0:20190128131017p:plain
VNet統合

完了後はこちら。

f:id:pir0:20190128131209p:plain
VNet統合設定完了

Azure Database VNet追加

続いてAzure DatabaseのVNetルールに先程のサブネットを追加します。

f:id:pir0:20190128151012p:plain
VNetルール追加

ファイアウォール規則も追加してみました。

f:id:pir0:20190128151300p:plain
VNetルール追加後
SSLは簡略化のためオフにしてますが、要件に合わせて設定ください。  

接続確認

以下テストスクリプトを用意し、Webappsの公開ディレクトリ(wwwroot)直下に配置します。  

<?php

// データベース接続チェック
// 接続情報は予め仕込んだアプリケーション設定から取得しております。
$dbc = mysqli_connect(getenv('CUSTOMCONNSTR_db_host'), getenv('CUSTOMCONNSTR_db_user'), getenv('CUSTOMCONNSTR_db_password'), getenv('CUSTOMCONNSTR_db_name'));
//echo ($dbc);
if (!$dbc) {
die('db connection failed \n'.mysql_error());
}
echo('DB connect success!! <br>');

// userテーブルからユーザ名を取得する
$sql = "select user from user;";
if ($result = $dbc->query($sql)) {

while ($row = $result->fetch_assoc()) {
echo $row["user"]. "<br>";
}
// 結果セットを閉じる
$result->close();
}

if (mysql_close($dbc)){
echo 'disconnect success!! \n';
}
?>

  ブラウザでアクセスしてみます。

f:id:pir0:20190128151952p:plain
ブラウザ表示結果
ユーザ一覧が取得できてますね。

ちなみに外部からアクセスできないか確認してみます。

$ mysql -h vnetintegrationtest.mysql.database.azure.com -u vnetintegration@vnetintegrationtest -p
Enter password:
ERROR 9000 (HY000): Client with IP address 'x.x.x.x' is not allowed to connect to this MySQL server.

もちろんですが、できないですね。

2019/1/28時点でまだプレビュー段階ですが、GAが待ち遠しいです。 Webapps - Azure Database間をよりセキュアに接続したい要件などありましたら検討してみてはいかがでしょうか。