1923

都内と業界の隅っこで生活しているエンジニアのノート

Go で Azure App Configuration を使う

Go で Azure App Configuration。REST API を使って値を取得してみます。

docs.microsoft.com

認証

何はともあれ認証です。HMAC と Azure Active Directory (Azure AD) がサポートされていますので両方試してみます。

docs.microsoft.com

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 トリガー を処理します。

docs.microsoft.com

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に入ってくる値を良い感じに処理する必要がありそうですね。

f:id:taka1923:20210919191601p:plain