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

Application Gatewayをエンドポイントとして、そのバックエンドにFrontDoor, App Serviceと続くのですが、
- なんだこの構成...
- 悪手じゃないか...
- コストが無駄にかかるだけじゃん...
と思う方もいらっしゃるかと思います。
ただ、世の中にはこういう要件を必要とされる方もいらっしゃるかもしれません。
それまではFrontDoorをエンドポイントとして運用し、App Serviceをバックエンドとする構成が最近のスタンダードかなと思います。
急遽上記の要件を満たすためにApplicationGatewayを導入した際のポイントをまとめたいと思います。
FrontDoorの設定
特殊な設定は不要です。 Application Gatewayからのトラフィックを受信できるよう、カスタムドメインを設定しておけば良いです。
1つ留意事項としては、FrontDoorにて受け付けるトラフィックをApplication Gatewayに制限することは難しそうです。
Application Gateway自体の送信元IPアドレスが変更される可能性があるためです。
また、FrontDoorにWAFを追加し、カスタムルールでIP制限をかけようと試みましたが、ダメでした。
Application Gatewayの設定
こちらも特殊な設定は不要です。 カスタムドメインを設定しつつ、フロントエンドIP、リスナー、ルール、HTTP設定、バックエンドプール、カスタムプローブの各コンポーネントを設定していけば問題ないです。
ポータルから設定する learn.microsoft.com コンポーネントについて
個人的に留意したのは以下の通りです。
- バックエンドプールに設定する際はFQDNをFrontDoorで設定したカスタムドメインを指定する
- バックエンド設定の追加にて
ホスト名をオーバーライドするの選択でバックエンドターゲットからホスト名を選択するを選択 - 一通り設定した後バックエンド正常性を確認すると異常のステータスとなるが、これはサポートに問い合わせたところ現状はこのままで良いとのこと。FrontDoorとはIPv6ベースでチェックしているようだが、portal上ではNG扱いになるとのことだとか。
そのほか
前段にApplication Gatewayにすることで構成が少し複雑になりました。
App Serviceでクライアントが要求したhostを取得する場合は x-original-host を取得しましょう。
意識しないとFrontDoorのヘッダーが取得され意図しない挙動が発生する可能性があるので要注意です。
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
サンプル
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 エンティティIDと応答URLをそれぞれ登録します。
- エンティティID
https://hoge.com/php-saml/demo1/metadata.php - 応答URL
https://hoge.com/php-saml/demo1/index.php/?acs
基本的なSAML構成
- エンティティID
登録後、SAML署名証明書から
フェデレーション メタデータ XMLをダウンロードします。 ダウンロード完了したら、エディタで開き、<X509Certificate>タグで囲まれた文字列をコピーし、前述のSPの設定を行うで作成したsettings.phpのx509certの欄にペーストします。完了後、 Azure Potalに戻り
demoのセットアップに表示されている文字列をsettings.phpにコピペします。
demoのセットアップ - ログイン URL
singleLogoutServiceのurl - Azure AD 識別子
entityIdのurl - ログアウト URL
singleLogoutServiceのurl
- ログイン URL
ユーザ登録を行う
このままでは誰一人、デモサイトを利用できるユーザがいないので、ユーザ登録を行います。
エンタープライズ アプリケーションのページから ユーザとグループ ブレードを開き、 +ユーザまたははグループの追加 をクリックし、Azure ADに既に登録されいているユーザの中から利用させたいユーザを登録します。

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

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

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

おわりに
構築するのに非常に苦労したのですが、いざ出来上がるとそんなに複雑ではなかったです。
これを利用すると例えば自社内で利用する独自のアプリケーション(wikiだったり、ポータルサイトなど)をAzure ADと連携させて認証させることで堅牢なシステムを作ることができそうですね。
特定のURLのみBasic認証を無効にする
テスト環境や、特定のユーザのみにサイトを公開したいときにBasic認証等でアクセス制限を掛けるケースがあると思いますが、一部のURLのみBasic認証を無効にしたいとの要望がありましたのでその時の設定方を記録しておきます。
- 前提
Basic認証は公開ディレクトリ配下全体に設定している
WEBアプリケーションフレームワークとしてLaravelを利用している
.htaccessに対する設定とする
apacheは2.2系とする
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
期待通りになっている。

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

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

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

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

完了後はこちら。

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

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

接続確認
以下テストスクリプトを用意し、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';
}
?>
ブラウザでアクセスしてみます。

ちなみに外部からアクセスできないか確認してみます。
$ 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間をよりセキュアに接続したい要件などありましたら検討してみてはいかがでしょうか。