- 追加された行はこの色です。
- 削除された行はこの色です。
#author("2020-12-12T17:05:33+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 及び 関数アプリからプライベートリンク経由でアクセスを行う。
最初に注意点を記載しておく。
- PostgreSQLの価格プランは 「汎用目的」 または 「メモリ最適化」 でないとプライベートリンクは利用できない。&color(red){(Basicでは利用できない)};
- 関数アプリ等から接続する場合は、PostgreSQLの接続のセキュリティで 「Azureサービスへのアクセスを許可」 を ON にする。
#html(</div>)
* 料金 [#y044c341]
#html(<div class="pl10">)
https://azure.microsoft.com/ja-jp/pricing/details/private-link/
| サービス/詳細 | 料金 |h
| Private Link サービス | Private Link サービスに料金はかかりません |
| プライベート エンドポイント | ¥ 1.12 / 時間 |
| 受信データ処理量 | ¥ 1.12/GB |
| 送信データ処理量 | ¥ 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]
** ユーザ 及び データベースの作成 [#s11e8263]
#html(<div class="pl10">)
Cloud Shell や VM からアクセスする為、Azureサービスへの
#html(<div class="images">)
#TODO
#html(</div>)
VMから PostgreSQLサーバに接続して、動作確認用のデータベース 及び DBユーザ を作成する。
#html(</div>)
VMのIP確認
#myterm2(){{
az vm list-ip-addresses -o table
VirtualMachine PublicIPAddresses PrivateIPAddresses
---------------- ------------------- --------------------
XXXXXXXX XX.XX.XXX.XXX 10.1.1.5
}}
** ユーザ 及び データベースの作成 [#s11e8263]
#html(<div class="pl10">)
VMにSSH接続
#myterm2(){{
ssh -i ./pem/id_rsa_XXXXXXXXX sampleuser@XX.XX.XXX.XXX
}}
VMにSSH接続して 以下の通り DB 及び ユーザを作成する。
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>)
#html(</div>)
#TODO
* PostgreSQL接続のセキュリティの設定 [#a7c4097b]
#html(<div class="pl10">)
下図の通り設定する。
#html(<div class="images">)
#ref(pg07.png,nolink);
#html(</div>)
* Private Link の構成 [#w4a9dd66]
VMからプライベートリンク経由で接続できるか確認しておく。
#myterm2(){{
psql "host=PostgreSQLサーバ名.privatelink.postgres.database.azure.com port=5432 dbname=sampledb1 user=user1@PostgreSQLサーバ名 password=DBユーザのパスワード sslmode=require"
}}
#html(</div>)
* 関数アプリの作成 [#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
# 関数アプリのプラン名
funcPlanName=${PREFIX}plan
funcPlanSku=B1
# 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
# 関数アプリの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
}}
#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"}}
}}
#html(</div>)