概要 †プライベートリンクを使用して Azure Database for PostgreSQL にアクセスする方法について記載する。 最初に注意点を記載しておく。
料金 †https://azure.microsoft.com/ja-jp/pricing/details/private-link/
目次 †
VNet(仮想ネットワーク) 及び VM(仮想マシン) の作成 †0_env.sh #!/bin/bash # リソース名の接頭文字 PREFIX=XXXXXXXXXXX # サブスクリプションID subscriptionId=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX # リージョン region=japanwest resourceGroup=${PREFIX}Resources vnetName=${PREFIX}VNet vnetPrefix=10.1.0.0/16 nsgName=${vnetName}SecGrp nsgPubRuleName=${nsgName}PubRule nsgPubInboundPort="8086" vmSubnetName=${PREFIX}VmSubnet vmSubnetPrefix=10.1.1.0/24 # 不要 #exSubnetName=${PREFIX}ExSubnet #exSubnetPrefix=10.1.2.0/24 vmName=${PREFIX}Vm vmImage=UbuntuLTS # "az vm image list -o table" で利用可能なイメージの一覧を確認可能 vmIpAddress=10.1.1.5 vmUser=sampleuser 1_resources.sh #!/bin/bash # 設定の読み込み source 0_env.sh # リソース作成 if [ "$1" == "--create" ]; then # リソースグループの作成 echo az group create az group create --name $resourceGroup --location $region # NSG(ネットワークセキュリティグループの)作成 echo az network nsg create az network nsg create --resource-group $resourceGroup --name $nsgName # NSGルール(Public) SSH echo "az network nsg rule create(ssh)" az network nsg rule create \ --resource-group $resourceGroup --nsg-name $nsgName --name ${nsgPubRuleName}2 \ --access Allow --protocol Tcp --direction Inbound --priority 100 \ --source-address-prefix Internet --source-port-range "*" --destination-port-range "22" # 仮想ネットワーク 及び サブネット作成 echo az network vnet create az network vnet create \ --name $vnetName --resource-group $resourceGroup \ --address-prefixes $vnetPrefix --network-security-group $nsgName \ --subnet-name $vmSubnetName --subnet-prefixes $vmSubnetPrefix # VNet統合用のサブネット (不要) #echo az vnet subnet create #az network vnet subnet create \ # --name $exSubnetName \ # --resource-group $resourceGroup \ # --vnet-name $vnetName \ # --address-prefixes $exSubnetPrefix # ユーザディレクトリ配下のSSH鍵をバックアップ bk_suffix=`date +%Y%m%d%H%M%S` if [ -e ~/.ssh/id_rsa ]; then mv ~/.ssh/id_rsa ~/.ssh/id_rsa_${bk_suffix} fi if [ -e ~/.ssh/id_rsa.pub ]; then mv ~/.ssh/id_rsa.pub ~/.ssh/id_rsa.pub_${bk_suffix} fi # 仮想マシンの作成 echo az vm create az vm create \ --resource-group $resourceGroup --name $vmName --image $vmImage --generate-ssh-keys \ --vnet-name $vnetName --subnet $vmSubnetName \ --private-ip-address $vmIpAddress --admin-username $vmUser \ --public-ip-address-dns-name `echo $vmName | tr '[A-Z]' '[a-z]'` # ポート開放 #az vm open-port --resource-group $resourceGroup --name $vmName --port 80 # 生成されたSSH鍵を移動( --ssh-dest-key-path が効かない為 ) mkdir -p pem if [ -e ~/.ssh/id_rsa ]; then mv ~/.ssh/id_rsa ./pem/id_rsa_${vmName} fi if [ -e ~/.ssh/id_rsa.pub ]; then mv ~/.ssh/id_rsa.pub ./pem/id_rsa_${vmName}.pub fi # バックアップしたSSH鍵を戻す if [ -e ~/.ssh/id_rsa_${bk_suffix} ]; then mv ~/.ssh/id_rsa_${bk_suffix} ~/.ssh/id_rsa fi if [ -e ~/.ssh/id_rsa.pub_${bk_suffix} ]; then mv ~/.ssh/id_rsa.pub_${bk_suffix} ~/.ssh/id_rsa.pub fi fi # リソース削除 if [ "$1" == "--delete" ]; then echo az group delete az group delete --name $resourceGroup fi 上記のシェルで VNet 及び VMを作成 ./1_resources_vm.sh --create データベース作成 †PostgreSQLサーバの作成 †Azure Portal から 「Azure Database for PostgreSQL」 を以下の通り作成した。 ユーザ 及び データベースの作成 †VMから PostgreSQLサーバに接続して、動作確認用のデータベース 及び DBユーザ を作成する。 VMのIP確認 az vm list-ip-addresses -o table VirtualMachine PublicIPAddresses PrivateIPAddresses ---------------- ------------------- -------------------- XXXXXXXX XX.XX.XXX.XXX 10.1.1.5 VMにSSH接続 ssh -i ./pem/id_rsa_XXXXXXXXX sampleuser@XX.XX.XXX.XXX postgres クライアントをインストール sudo apt update sudo apt install -y postgresql-client DB 及び ユーザを作成する。 psql "host=サーバ名.postgres.database.azure.com port=5432 dbname=postgres user=ユーザ名@サーバ名 password=管理者アカウントのパスワード sslmode=require" psql (10.15 (Ubuntu 10.15-0ubuntu0.18.04.1), server 11.6) : postgres=> CREATE DATABASE sampledb1 ENCODING UTF8; CREATE DATABASE postgres=> CREATE USER user1 WITH PASSWORD 'DBユーザのパスワード'; CREATE ROLE postgres=> GRANT ALL ON DATABASE sampledb1 TO user1; GRANT postgres=> \q 作成したユーザで接続し直してアクセス確認用のテーブルを作成しておく psql "host=サーバ名.postgres.database.azure.com port=5432 dbname=sampledb1 user=user1@サーバ名 password=DBユーザのパスワード sslmode=require" : postgres=> create table sampletable1 (id integer, col1 integer, col2 integer, constraint sampletable1_pkc primary key (id)); CREATE TABLE postgres=> insert into sampletable1 values(1, 10, 100); INSERT 0 1 postgres=> insert into sampletable1 values(2, 20, 200); INSERT 0 1 postgres=> \q プライベートエンドポイント/プライベートリンクの作成 †下図の通り プライベートエンドポイント を作成する。 リソースの種類: 「Microsoft.DBforPostgreSQL/servers」、リソースは作成したPostgresサーバ 仮想ネットワーク 及び サブネットは、作成済みのサブネットを選択。 PostgreSQL接続のセキュリティの設定 †下図の通り設定する。 VMからプライベートリンク経由で接続できるか確認しておく。 psql "host=PostgreSQLサーバ名.privatelink.postgres.database.azure.com port=5432 dbname=sampledb1 user=user1@PostgreSQLサーバ名 password=DBユーザのパスワード sslmode=require" 関数アプリの作成 †
0_env_func.sh #!/bin/bash source 0_env.sh # DB接続情報 (ホスト名には作成したプライベートリンクを使用) DB_HOST=作成したPostgresサーバ名.privatelink.postgres.database.azure.com DB_PORT=5432 DB_NAME=DB名 DB_USER=DBユーザ名@作成したPostgresサーバ名 DB_PW=DBユーザのパスワード # Insightのリージョン insightsRegion=japaneast # リソースグループ resourceGroupOrg=${resourceGroup} resourceGroup=${resourceGroupOrg}Func # ストレージアカウント名 storageAccountName=${PREFIX}straccount storageSku=Standard_LRS # 関数アプリ名 funcAppName=${PREFIX}SampleFunc # 使用するFunctionsのバージョン funcVersion=2 # 関数アプリのプラン名 funcPlanName=${PREFIX}plan funcPlanSku=B1 # Application insights insightsName=${PREFIX}Insights insightsDays=30 insightsSetting="" if [ "$subscriptionId" != "" ]; then insightsSetting="--app-insights $insightsName" fi 2_resources_func.sh #!/bin/bash source ./0_env_func.sh # 関数アプリのデプロイ deployfunc() { cd functions exefile=`cat host.json | grep defaultExecutablePath | awk '{print $2}' | sed 's/"//g'` rm -rf $exefile GOOS=windows GOARCH=amd64 go build -o $exefile zip -r ../functions.zip * cd ../ az functionapp deployment source config-zip -g $resourceGroup -n $funcAppName --src functions.zip rm -rf functions.zip # 関数アプリのURLを表示 code=`az functionapp keys list -n $funcAppName -g $resourceGroup | grep masterKey | awk '{print $2}' | sed -E 's/("|,)//g'` funcAppNameLower=`echo $funcAppName | tr '[:upper:]' '[:lower:]'` echo "SampleFunc URL ... https://${funcAppNameLower}.azurewebsites.net/api/SampleFunc?code=${code}" } # リソースの作成 if [ "$1" == "--create" ]; then #------------------------------- # リソースグループの作成 #------------------------------- echo az group create az group create --name $resourceGroup --location $region #------------------------------- # ストレージアカウントの作成 #------------------------------- echo az storage account create az storage account create \ --name $storageAccountName \ --location $region \ --resource-group $resourceGroup \ --sku $storageSku #------------------------------- # Application Insights 拡張が利用できない場合は追加インストール #------------------------------- x=`az monitor app-insights --help 2>&1` if [ "$?" != "0" ]; then az extension add -n application-insights fi #------------------------------- # Application Insights コンポーネント作成 #------------------------------- echo az monitor app-insights component create if [ "$subscriptionId" != "" ]; then az monitor app-insights component create \ --app $insightsName \ --location $insightsRegion \ --resource-group $resourceGroup \ --query-access Enabled \ --retention-time $insightsDays \ --subscription $subscriptionId fi #------------------------------- # 関数アプリ #------------------------------- # 関数プランの作成 echo az functionapp plan create az functionapp plan create \ --name $funcPlanName \ --resource-group $resourceGroup \ --location $region \ --sku $funcPlanSku # 関数アプリの作成 echo az functionapp create az functionapp create \ --name $funcAppName \ --storage-account $storageAccountName \ --plan $funcPlanName \ --resource-group $resourceGroup \ --functions-version $funcVersion $insightsSetting # 関数アプリの環境変数の設定 echo "az functionapp config appsettings" az functionapp config appsettings set \ --name $funcAppName \ --resource-group $resourceGroup \ --settings "DB_HOST=${DB_HOST}" "DB_PORT=${DB_PORT}" "DB_NAME=${DB_NAME}" "DB_USER=${DB_USER}" "DB_PW=${DB_PW}" ## 関数アプリのVNet統合(当コマンドはプレビュー版の為、将来で変更/削除される可能性あり) #echo az functionapp vnet-integration add #az functionapp vnet-integration add \ # --name $funcAppName \ # --resource-group $resourceGroup \ # --vnet $vnetName \ # --subnet $exSubnetName #------------------------------- # 関数アプリのデプロイ #------------------------------- # すぐにデプロイするとエラー(Timeout)になる場合がある為、少し待つ echo "sleep 10 seconds..." sleep 10 deployfunc fi # 関数アプリのデプロイのみ if [ "$1" == "--deployfunc" ]; then deployfunc fi # リソースの削除 if [ "$1" == "--delete" ]; then az group delete --name $resourceGroup -y #az group delete --name ${resourceGroupOrg}App -y fi functions/host.json { "version": "2.0", "httpWorker": { "description": { "defaultExecutablePath": "server.exe" } }, "extensions": { "queues": { "batchSize": 16, "maxDequeueCount": 5, "newBatchThreshold": 8 } }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[1.*, 2.0.0)" } } functions/server.go package main import ( "encoding/json" "fmt" "log" "net/http" "os" "time" "database/sql" _ "github.com/lib/pq" ) type ReturnValue struct { Data string } type InvokeResponse struct { Result map[string]interface{} } type InvokeRequest struct { Data map[string]interface{} Metadata map[string]interface{} } func printDebug(format string, params ...interface{}){ log.SetOutput(os.Stdout) msg := fmt.Sprintf(format, params...) log.Printf("[DEBUG] %s\n", msg) } func printInfo(format string, params ...interface{}){ log.SetOutput(os.Stdout) msg := fmt.Sprintf(format, params...) log.Printf("[INFO] %s\n", msg) } func printError(format string, params ...interface{}){ log.SetOutput(os.Stderr) msg := fmt.Sprintf(format, params...) log.Printf("[ERROR] %s\n", msg) log.SetOutput(os.Stdout) } func init(){ log.SetOutput(os.Stdout) log.SetFlags(0) } /** * 環境変数の取得. */ func getEnv(envName string, defaultValue string) string { value, exists := os.LookupEnv(envName) if exists { return value } else { return defaultValue } } /** * DBのデータを読み取る. */ func sampleFuncHandler(w http.ResponseWriter, r *http.Request) { printInfo("START sampleFuncHandler") defer func(){ err := recover() if err != nil { panic(fmt.Sprintf("ERROR sampleFuncHandler, %v\n", err)); } else { printInfo("END sampleFuncHandler") } }() // DBからデータを読み取り results := selectData() // レスポンスデータ設定 invokeResponse := InvokeResponse{Result: results} js, err := json.Marshal(invokeResponse) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(js) } /** * DBクライアント取得. */ func getDbClient() *sql.DB { HOST := getEnv("DB_HOST", "localhost") PORT := getEnv("DB_PORT", "5432") DATABASE := getEnv("DB_NAME", "db") USER := getEnv("DB_USER", "user") PASSWORD := getEnv("DB_PW" , "pwd") var connectionString string = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=require", HOST, PORT, USER, PASSWORD, DATABASE) db, err := sql.Open("postgres", connectionString) if err != nil { panic(err) } return db } /** * データ登録(InfluxDB). */ func selectData() map[string]interface{} { printInfo("START selectData") result := make(map[string]interface{}, 0) // DB接続 client := getDbClient() defer func(){ err := recover() if client != nil { client.Close() } if err != nil { panic(err) } }() sql_statement := "SELECT id, col1, col2 from sampletable1" rows, err := client.Query(sql_statement) if err != nil { panic(err) } items := make([]map[string]interface{}, 0) for rows.Next() { var id int var col1 int var col2 int switch err := rows.Scan(&id, &col1, &col2); err { case sql.ErrNoRows: printDebug("No rows were returned") break case nil: printDebug("Data row = (%v, %v, %v, %v)\n", id, col1, col2) rec := make(map[string]interface{}, 0) rec["id"] = id rec["col1"] = col1 rec["col2"] = col2 items = append(items, rec) default: if err != nil { panic(err) } } } result["time"] = time.Now().Format("2006-01-02 15:04:05") result["items"] = items rows.Close() printInfo("END selectData") return result } func main() { httpInvokerPort, exists := os.LookupEnv("FUNCTIONS_HTTPWORKER_PORT") if exists { printInfo("FUNCTIONS_HTTPWORKER_PORT: " + httpInvokerPort) } mux := http.NewServeMux() mux.HandleFunc("/SampleFunc", sampleFuncHandler) log.Println("Go server Listening...on httpInvokerPort:", httpInvokerPort) log.Fatal(http.ListenAndServe(":"+httpInvokerPort, mux)) } functions/SampleFunc/function.json { "bindings": [ { "type": "httpTrigger", "direction": "in", "name": "req", "methods": [ "get", "post" ] }, { "type": "http", "direction": "out", "name": "res" } ] } functions/local.settings.json { "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "DB_HOST": "xxxxxxxxxxxx.postgres.database.azure.com", "DB_PORT": "5432", "DB_NAME": "sampledb1", "DB_USER": "user1@xxxxxxxxxxxx", "DB_PW": "xxxxxxxxxxxx" } } 上記のファイルを作成し、以下を実行。 ./2_resources_func.sh --create : : SampleFunc URL ... https://XXXXXXXX.azurewebsites.net/api/SampleFunc?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 上記で表示されたURLにアクセスして動作確認。 curl https://XXXXXXXX.azurewebsites.net/api/SampleFunc?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX {"Result":{"items":[{"col1":10,"col2":100,"id":1},{"col1":20,"col2":200,"id":2}],"time":"2020-12-12 20:53:38"}} |