Azure Functions カスタム ハンドラー + Golang でマネージド ID を使用して Azure Database for MySQL に接続する
Azure Functions のカスタム ハンドラー の Timer trigger で Golang のプログラムを実行。 マネージド IDを使用して、Azure Database for MySQL のデータベースへの接続を試してみます。
Function App と MySQL のデータベースは作成済み。MySQLのファイアウォールの設定が終わっている程度の状態です。
準備
マネージドID作成して、Function Appに割り当て
az identity create --resource-group TESTRG --name TESTIDENTITY $resourceID=$(az identity show --resource-group TESTRG --name TESTIDENTITY --query id --output tsv) az functionapp identity assign --resource-group TESTRG --name TESTFUNC --identities $resourceID
MySQLユーザー作成時にクライアントIDを使うのでメモ
az identity show --resource-group Testing --name testscgo1220a1Identity --query clientId --output tsv
MySQL サーバー に Active Directory 管理者を設定
ポータルから Azure Database for MySQL サーバーの [Active Directory 管理者] > [管理者の設定] で設定できます。
Azure CLI で以下のコマンドで実行したときは結構時間がかかり、ポータルの方が早かった感じです。
$objectID=$(az ad user list --display-name "DISPLAYNAME" --query [].objectId --output tsv) az mysql server ad-admin create --server-name TESTMYSQLSRV --resource-group TESTRG --display-name "DISPLAYNAME" --object-id $objectID
Command group 'mysql server ad-admin' is in preview. It may be changed/removed in a future release.
マネージド ID の MySQL ユーザーを作成してデータベースへの権限を設定
上記で設定した Azure AD 管理者ユーザーで MySQL データベースに接続し、先ほど作成したマネージド ID の MySQL ユーザー(user1)を作成します。ついでにデータベースの権限も適当に設定しておきます。
SET aad_auth_validate_oids_in_tenant = OFF; CREATE AADUSER 'user1' IDENTIFIED BY 'クライアントID'; GRANT SELECT,INSERT,UPDATE,DELETE ON TESTMYSQLDB.* TO user1@'%'
MySQL Workbench で接続するときは、Advanced の Enable Cleartext Authentication Plugin にチェックします。パスワードに使用するトークンは以下で取得。
az account get-access-token --resource-type oss-rdbms
実装
helloフォルダを作成して、Timer Trigger の設定を記述した function.jsonを入れておけば、設定した時間(今回は毎時30分)に、"/hello" が呼ばれますので、その中に処理を記載します。
MySQLの接続は、Go を使用してトークンを取得するを参考にローカル トークン サービスからMySQLのアクセストークンを取得して、MySQLのパスワードに設定しています。アクセストークンの有効期限については無配慮です。
https://docs.microsoft.com/en-us/azure/mysql/howto-configure-ssl
hello/function.json
{ "bindings": [ { "schedule": "0 30 * * * *", "name": "myTimer", "type": "timerTrigger", "direction": "in" } ] }
main.go
package main import ( "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "io/ioutil" "log" "net/http" "net/url" "os" "time" ) type InvokeResponse struct { Outputs map[string]interface{} Logs []string ReturnValue interface{} } type ReturnValue struct { Data string } type Hello struct { gorm.Model Value string DateTime time.Time } type responseJson struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn string `json:"expires_in"` ExpiresOn string `json:"expires_on"` NotBefore string `json:"not_before"` Resource string `json:"resource"` TokenType string `json:"token_type"` } func main() { customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT") if exists { fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort) } mux := http.NewServeMux() mux.HandleFunc("/hello", helloHandler) fmt.Println("Go server Listening...on FUNCTIONS_CUSTOMHANDLER_PORT:", customHandlerPort) log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux)) } // Timer Trigger Event func helloHandler(w http.ResponseWriter, r *http.Request) { db, err := sqlConnect() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer db.Close() user := Hello{ Value: "Hello", DateTime : time.Now() } db.NewRecord(user) db.Create(&user) returnValue := ReturnValue{Data: "return val"} outputs := make(map[string]interface{}) outputs["output1"] = "output1" invokeResponse := InvokeResponse{outputs, []string{ "log1" }, returnValue} json, err := json.Marshal(invokeResponse) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(json) } func sqlConnect() (database *gorm.DB, err error) { json, err := getToken() if err != nil { return nil, err } fmt.Println("sqlConnect...") DBMS := "mysql" USER := "user1@TESTMYSQLSRV" PASS := json.AccessToken DB := "tcp(TESTMYSQLSRV.mysql.database.azure.com:3306)" DBNAME := "TESTMYSQLDB" CONNECT := fmt.Sprintf("%s:%s@%s/%s?tls=custom&parseTime=true&allowCleartextPasswords=1",USER , PASS, DB, DBNAME) rootCertPool := x509.NewCertPool() pem, err := ioutil.ReadFile("./BaltimoreCyberTrustRoot.crt.pem") if err != nil { return nil, err } if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { return nil, errors.New(fmt.Sprint("Failed to append PEM.")) } mysql.RegisterTLSConfig("custom", &tls.Config{RootCAs: rootCertPool}) db, err := gorm.Open(DBMS, CONNECT) if err != nil { return nil, err } fmt.Println("db connected: ", &db) return db, nil } // トークン取得 func getToken() (responseJson, error) { var r responseJson // Create HTTP request for a managed services for Azure resources token to access Azure Resource Manager vaultURL := fmt.Sprintf("%s?resource=%s&api-version=2019-08-01&client_id=%s", os.Getenv("IDENTITY_ENDPOINT"), "https://ossrdbms-aad.database.windows.net", "クライアントID") msiEndpoint, err := url.Parse(vaultURL) if err != nil { return r, err } req, err := http.NewRequest("GET", msiEndpoint.String(), nil) if err != nil { return r, err } req.Header.Add("X-IDENTITY-HEADER", os.Getenv("IDENTITY_HEADER")) // Call managed services for Azure resources token endpoint client := &http.Client{} resp, err := client.Do(req) if err != nil{ return r, err } // Pull out response body responseBytes, err := ioutil.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return r, err } if resp.StatusCode >= 400 { return r, fmt.Errorf("bad response status code %d", resp.StatusCode) } // Unmarshall response body into struct err = json.Unmarshal(responseBytes, &r) if err != nil { return r, err } return r, nil }