Go で Azure App Configuration。REST API を使って値を取得してみます。
認証
何はともあれ認証です。HMAC と Azure Active Directory (Azure AD) がサポートされていますので両方試してみます。
HMAC
コードはHMAC 認証 (Golang)とほぼ同じで、Azure AD 認証と切り替えするために interface を揃えています。
type Config struct { id string secret string } func New(id string, secret string) *Config { return &Config{ id: id, secret: secret, } } func (c *Config) SignRequest(req *http.Request) error { method := req.Method host := req.URL.Host pathAndQuery := req.URL.Path if req.URL.RawQuery != "" { pathAndQuery = pathAndQuery + "?" + req.URL.RawQuery } content, err := ioutil.ReadAll(req.Body) if err != nil { return err } req.Body = ioutil.NopCloser(bytes.NewBuffer(content)) key, err := base64.StdEncoding.DecodeString(c.secret) if err != nil { return err } timestamp := time.Now().UTC().Format(http.TimeFormat) contentHash := getContentHashBase64(content) stringToSign := fmt.Sprintf("%s\n%s\n%s;%s;%s", strings.ToUpper(method), pathAndQuery, timestamp, host, contentHash) signature := getHmac(stringToSign, key) req.Header.Set("x-ms-content-sha256", contentHash) req.Header.Set("x-ms-date", timestamp) req.Header.Set("Authorization", "HMAC-SHA256 Credential="+c.id+", SignedHeaders=x-ms-date;host;x-ms-content-sha256, Signature="+signature) return nil } func getContentHashBase64(content []byte) string { sha := sha256.New() sha.Write(content) return base64.StdEncoding.EncodeToString(sha.Sum(nil)) } func getHmac(content string, key []byte) string { hmac := hmac.New(sha256.New, key) hmac.Write([]byte(content)) return base64.StdEncoding.EncodeToString(hmac.Sum(nil)) }
Azure Active Directory (Azure AD)
マネージド ID を使用して App Configuration にアクセスする を参考にマネージド IDの設定を行います。
あとは、Azure ADからトークンを取得して、HTTP リクエストの Bearer に設定するだけです。ローカル トークン サービスから、今回使用する App Configuration リソースのトークンを取得しています。
type Config struct { endpoint string } 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 New(endpoint string) *Config { return &Config{ endpoint: endpoint, } } func (c *Config) SignRequest(req *http.Request) error { json, err := getToken(c.endpoint) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+json.AccessToken) return nil } // トークン取得 func getToken(endpoint string) (*responseJson, error) { // 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", os.Getenv("IDENTITY_ENDPOINT"), endpoint) msiEndpoint, err := url.Parse(vaultURL) if err != nil { return nil, err } req, err := http.NewRequest("GET", msiEndpoint.String(), nil) if err != nil { return nil, 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 nil, err } // Pull out response body responseBytes, err := ioutil.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err } if resp.StatusCode >= 400 { return nil, fmt.Errorf("bad response status code %d", resp.StatusCode) } // Unmarshall response body into struct var res responseJson err = json.Unmarshal(responseBytes, &res) if err != nil { return nil, err } return &res, nil }
実行
Azure AD 認証をAzure Functions のカスタム ハンドラーで試してみます。Gin を使って HTTP トリガー を処理します。
func main() { customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT") if !exists { customHandlerPort = "7071" } fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort) // Azure AD 認証 auth := authAad.New("https://XXXXXX.azconfig.io") s := New(auth, "https://XXXXXX.azconfig.io") r := gin.Default() r.GET("/func1", s.Func1) r.Run(":" + customHandlerPort) } type apiResponse struct { Etag string `json:"etag"` Key string `json:"key"` Label interface{} `json:"label"` ContentType string `json:"content_type"` Value string `json:"value"` Tags struct { } `json:"tags"` Locked bool `json:"locked"` LastModified time.Time `json:"last_modified"` } type auth interface { SignRequest(req *http.Request) error } type Service struct { auth auth endpointUrl string } func New(auth auth, endpoint string) *Service { return &Service{ auth: auth, endpointUrl: endpoint + "/kv/%v?api-version=%v", } } func (s *Service) getValue(key string) (*apiResponse, error) { u := fmt.Sprintf(s.endpointUrl, key, "1.0") req, err := http.NewRequest(http.MethodGet, u, bytes.NewBuffer([]byte(""))) if err != nil { return nil, err } if err := s.auth.SignRequest(req); err != nil { return nil, err } client := new(http.Client) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() byteArray, _ := ioutil.ReadAll(resp.Body) var apiRes apiResponse json.Unmarshal(byteArray, &apiRes) return &apiRes, nil } func (s *Service) Func1(c *gin.Context) { r, err := s.getValue("AppConfiguration1:Settings:Key1") if err != nil { c.JSON(http.StatusBadGateway, err) } c.JSON(http.StatusOK, r.Value) }
値を取得できました。 また、「auth := ~」部分を差し替えて HMAC認証 でも取得できました。
実際に使うにはキャッシュを考慮したり、機能フラグはValueに入ってくる値を良い感じに処理する必要がありそうですね。