概要

プライベートリンクを使用して Azure Database for PostgreSQL にアクセスする方法について記載する。
尚、この記事では VM 及び 関数アプリからプライベートリンク経由でアクセスを行う。

最初に注意点を記載しておく。

  • PostgreSQLの価格プランは 「汎用目的」 または 「メモリ最適化」 でないとプライベートリンクは利用できない。(Basicでは利用できない)
  • 関数アプリ等から接続する場合は、PostgreSQLの接続のセキュリティで 「Azureサービスへのアクセスを許可」 を ON にする。

料金

https://azure.microsoft.com/ja-jp/pricing/details/private-link/

サービス/詳細料金
Private Link サービスPrivate Link サービスに料金はかかりません
プライベート エンドポイント¥ 1.12 / 時間
受信データ処理量¥ 1.12/GB
送信データ処理量¥ 1.12/GB

目次

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」 を以下の通り作成した。
※ 汎用(General Purpose) または メモリ最適化(Memory Optimized) 以上の価格レベルにする事。
「Basic」 では Private Link 機能は利用できない。
https://docs.microsoft.com/ja-jp/azure/postgresql/concepts-data-access-and-security-private-link

pg01.png

ユーザ 及び データベースの作成

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

プライベートエンドポイント/プライベートリンクの作成

下図の通り プライベートエンドポイント を作成する。

pg02.png
pg03.png

 
インスタンス名などを入力。

pg04.png

リソースの種類: 「Microsoft.DBforPostgreSQL/servers」、リソースは作成したPostgresサーバ
ターゲット サブリソースは [postgresqlServer] を選択。

pg05.png

仮想ネットワーク 及び サブネットは、作成済みのサブネットを選択。

pg06.png

PostgreSQL接続のセキュリティの設定

下図の通り設定する。

pg07.png

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にアクセスして動作確認。
※関数アプリがプライベートリンクを使用してPostgreSQLにアクセスできる事を確認する。

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"}}

添付ファイル: filepg07.png 283件 [詳細] filepg06.png 291件 [詳細] filepg05.png 290件 [詳細] filepg04.png 297件 [詳細] filepg03.png 303件 [詳細] filepg02.png 290件 [詳細] filepg01.png 292件 [詳細]

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-12-13 (日) 16:32:23 (1229d)