#author("2020-12-12T17:05:33+00:00","","")
#author("2020-12-12T21:13:29+00:00","","")
#mynavi(Azureメモ)
#setlinebreak(on);

#html(){{
<style>
.images img {
  border: 1px solid #333;
}
</style>
}}


* 概要 [#b8d6cc43]
#html(<div class="pl10">)
#TODO
プライベートリンクを使用して Azure Database for PostgreSQL にアクセスする方法について記載する。
尚、この記事では VM 及び 関数アプリからプライベートリンク経由でアクセスを行う事とする。
#html(</div>)

* 料金 [#y044c341]
#html(<div class="pl10">)

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

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

#html(</div>)


* 目次 [#m1734f6a]
#contents
- 参考
-- [[Azure Database for PostgreSQL 用の Private Link - 単一サーバー>https://docs.microsoft.com/ja-jp/azure/postgresql/concepts-data-access-and-security-private-link]]
-- [[ポータルを使用して Azure Database for PostgreSQL 単一サーバー用の Private Link を作成および管理する>https://docs.microsoft.com/ja-jp/azure/postgresql/howto-configure-privatelink-portal]]
-- [[CLI を使用して Azure Database for PostgreSQL 単一サーバー用の Private Link を作成および管理する>https://docs.microsoft.com/ja-jp/azure/postgresql/howto-configure-privatelink-cli]]

* VNet(仮想ネットワーク) 及び VM(仮想マシン) の作成 [#h9d49da3]
#html(<div class="pl10">)

#html(){{
<div id="tabs1">
  <ul>
    <li><a href="#tabs1-1">0_env.sh</a></li>
    <li><a href="#tabs1-2">1_resources.sh</a></li>
  </ul>
}}

// START tabs1-1
#html(<div id="tabs1-1">)

0_env.sh
#mycode2(){{
#!/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
# 不要
#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
}}

#html(</div>)
// END tabs1-1

// START tabs1-2
#html(<div id="tabs1-2">)

1_resources.sh
#mycode2(){{
#!/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
  # 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]'` \
    --custom-data 2_setup_vm.sh
    --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
}}

#html(</div>)
// END tabs1-2

#html(</div>)
// END tabs1

#html(<script>$(function() { $("#tabs1").tabs(); });</script>)

上記のシェルで VNet 及び VMを作成
#myterm2(){{
./1_resources_vm.sh --create
}}

#html(</div>)

* データベース作成 [#t958e7c9]
#html(<div class="pl10">)

** PostgreSQLサーバの作成 [#qbde4ed1]

#html(<div class="pl10">)
Azure Portal から 「Azure Database for PostgreSQL」 を以下の通り作成した。
※ 汎用(General Purpose) または メモリ最適化(Memory Optimized)  以上の価格レベルにする事。
※ &color(red){「Basic」 では Private Link 機能は利用できない。};
※ https://docs.microsoft.com/ja-jp/azure/postgresql/concepts-data-access-and-security-private-link

#html(<div class="images">)
#ref(pg01.png,nolink);
#html(</div>)

#html(</div>)


** Azureサービスへのアクセス許可 [#p077f6cc]
** PostgreSQL接続のセキュリティの設定 [#a7c4097b]
#html(<div class="pl10">)

Cloud Shell や VM からアクセスする為、Azureサービスへの
下図の通り設定する。
#html(<div class="images">)
#TODO
#ref(pg07.png,nolink);
#html(</div>)

#html(</div>)

** ユーザ 及び データベースの作成 [#s11e8263]
#html(<div class="pl10">)

VMにSSH接続して 以下の通り DB 及び ユーザを作成する。
VMから PostgreSQLサーバに接続して、動作確認用のデータベース 及び DBユーザ を作成する。

VMのIP確認
#myterm2(){{
az vm list-ip-addresses -o table
VirtualMachine    PublicIPAddresses    PrivateIPAddresses
----------------  -------------------  --------------------
XXXXXXXX       XX.XX.XXX.XXX        10.1.1.5
}}

VMにSSH接続
#myterm2(){{
ssh -i ./pem/id_rsa_XXXXXXXXX sampleuser@XX.XX.XXX.XXX
}}

postgres クライアントをインストール
#myterm2(){{
sudo apt update
sudo apt install -y postgresql-client
}}

DB 及び ユーザを作成する。
#myterm2(){{
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
}}

作成したユーザで接続し直してアクセス確認用のテーブルを作成しておく
#myterm2(){{
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
}}

#html(</div>)


#html(</div>)

* プライベートエンドポイントの作成 [#bf41e9c2]
* プライベートエンドポイント/プライベートリンクの作成 [#bf41e9c2]
#html(<div class="pl10">)

Azure portal から [Private Link] を検索/選択後、[プライベートエンドポイントの作成] を選択。
下図の通り プライベートエンドポイント を作成する。

#html(<div class="images"><div class="ib" style="vertical-align: top;">)
#ref(pg02.png,nolink);
#html(</div><div class="ib" style="vertical-align: top;">)
#ref(pg03.png,nolink);
#html(</div><div class="ib" style="vertical-align: top;">)
 
インスタンス名などを入力。
#ref(pg04.png,nolink);
#html(</div><div class="ib" style="vertical-align: top;">)
リソースの種類: 「Microsoft.DBforPostgreSQL/servers」、リソースは作成したPostgresサーバ 
ターゲット サブリソースは [postgresqlServer] を選択。
#ref(pg05.png,nolink);
#html(</div><div class="ib" style="vertical-align: top;">)
仮想ネットワーク 及び サブネットは、作成済みのサブネットを選択。
#ref(pg06.png,nolink);
#html(</div></div>)



#TODO
#html(</div>)

* Private Link の構成 [#w4a9dd66]
* 関数アプリの作成 [#p1893103]
#html(<div class="pl10">)

#TODO
#html(){{
<div id="tabs2">
  <ul>
    <li><a href="#tabs2-1">0_env_func.sh</a></li>
    <li><a href="#tabs2-2">2_resources_func.sh</a></li>
    <li><a href="#tabs2-3">functions/host.json</a></li>
    <li><a href="#tabs2-4">functions/server.go</a></li>
    <li><a href="#tabs2-5">functions/SampleFunc/function.json</a></li>
    <li><a href="#tabs2-6">functions/local.settings.json</a></li>
  </ul>
}}

// START tabs2-1
#html(<div id="tabs2-1">)

0_env_func.sh
#mycode2(){{
#!/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

# 関数アプリのプラン名(VNet統合を使用するので Premium Plan )
funcPlanName=${PREFIX}plan
funcPlanSku=EP1

# Application insights
insightsName=${PREFIX}Insights
insightsDays=30
insightsSetting=""
if [ "$subscriptionId" != "" ]; then
  insightsSetting="--app-insights $insightsName"
fi
}}

#html(</div>)
// END tabs2-1

// START tabs2-2
#html(<div id="tabs2-2">)

2_resources_func.sh
#mycode2(){{
#!/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
}

# リソースの作成
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
    deploy_func

    # 関数アプリの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}"
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
}}

#html(</div>)
// END tabs2-2

// START tabs2-3
#html(<div id="tabs2-3">)

functions/host.json
#mycode2(){{
{
    "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)"
    }   
}
}}

#html(</div>)
// END tabs2-3

// START tabs2-4
#html(<div id="tabs2-4">)

functions/server.go
#mycode2(){{
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))
}
}}

#html(</div>)
// END tabs2-4

// START tabs2-5
#html(<div id="tabs2-5">)

functions/SampleFunc/function.json
#mycode2(){{
{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}
}}

#html(</div>)
// END tabs2-5

// START tabs2-6
#html(<div id="tabs2-6">)

functions/local.settings.json
#mycode2(){{
{
    "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"
    }   
}
}}
#html(</div>)
// END tabs2-6

#html(</div>)
// END tabs2

#html(<script>$(function() { $("#tabs2").tabs(); });</script>)

上記のファイルを作成し、以下を実行。
#myterm2(){{
./2_resources_func.sh --create
 :
 :
SampleFunc URL ... https://XXXXXXXX.azurewebsites.net/api/SampleFunc?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
}}

上記で表示されたURLにアクセスして動作確認。
※関数アプリがプライベートリンクを使用してPostgreSQLにアクセスできる事を確認する。

#myterm2(){{
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"}&#125;
}}

#html(</div>)



トップ   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS