1923

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

Azure と ASP.NET でメールフォームを作りました

PRです。最近は Go 中心のお仕事でしたが、今年は .NET に戻りつつありますのでメールフォーム的なものを .NET で作りました。 仕組みは簡単で以下の通りです。

  1. API でフォームの内容を受ける(Azure Functions)
  2. 受信内容を登録(Azure Cosmos DB)
  3. メール送信などの通知処理を実行(Azure Cosmos DB の Change Feed でトリガーされる Azure Functions)

入力項目、バリデーションや通知設定は SQL Database や Azure Table Storage に入れてあります。 また、API Management 使ったり、サービスエンドポイントで各サービスを接続したりして、構成的には以下のような感じで作りました。

構成図

  1. API Managementからマネージド ID で接続
  2. Cosmos DBのトリガーを使用
  3. サービスエンドポイントを使用して接続

Cosmos DB Change Feed や Azure Functions については、以下などを参考にさせていただきました。

blog.beachside.dev

blog.shibayan.jp

k-miyake.github.io

そのほか、

  • ドキュメントサイトを Blob と CDN
  • 管理サイトを Web Apps と Azure B2C
  • ソース管理やCI/CDは Azure DevOps

通知などは Teams と連携させて、工数やコストをあまりお金をかけない仕組みで動いています。

出来たのはこんな感じで普通のフォームです。

SWF ドキュメント に詳細は記載していますが、試しにフォーム作ってみたい方がいましたら、お声がけください。宣伝でした。

教育訓練給付制度の専門実践教育訓練給付金について

いつものAzure関連のネタとは関係ないですが、専門実践教育訓練給付金の申請手続きを行ったので、簡単なまとめです。

もともと、国も支援に力入れている分野ではあったと思うのですが、"リスキリング支援「5年で1兆円」"ということで、「リスキリング」とか「学び直し」など、最近の市場のテーマですね。

リスキリングとは「新しい職業に就くために、あるいは、今の職業で必要とされるスキルの大幅な変化に適応するために、必要なスキルを獲得する/させること」

経済産業省

専門実践教育訓練給付金については以下がわかりやすかったです。

www.gov-online.go.jp

今回手続きしました専門実践教育訓練を含む、教育訓練給付制度の指定教育訓練講座は以下のサイトで検索できます。

教育訓練給付制度 厚生労働大臣指定教育訓練講座 検索システム

さて、手続きです。

マイナンバーを記載するところもありますし、手続きや今後の支給申請ごとにマイナンバーカードを提示することで写真2枚の提出が省略できますので、持参する身分証明書はマイナンバーカードが便利です。そのため、私は身分証明書としてマイナンバーカードを使用しました。  

1.居住所を管轄しているハローワークを確認

居住所を管轄しているハローワークを調べます。居住地が東京の場合、東京ハローワークで管轄している市区町村を確認できます。

また、電話で確認したところ、支給対象者支給要件の確認は身分証明書を持って窓口に行く必要があり、予約などは不要でした。

2.受給資格の確認

身分証明書を持って管轄のハローワクーに訪問し、支給要件の確認をします。

相談窓口に書類を提出して調べて頂き、「教育訓練給付金支給要件回答書(専門実践教育訓練)」を受け取ります。 資格要件を満たしていましたので、今後の流れについても説明していただけました。

今後の流れ

  1. ジョブカードを作成
  2. 訓練対応キャリアコンサルティングを受ける
  3. ハローワークで申請(今回と同じ窓口)
3.訓練前キャリアコンサルティングの予約

ジョブカード作成支援予約専用サイトでキャリアコンサルティングを予約です。私のときは、1週間くらい先から予約可能で、予約可能な直近2週間では3-4割埋まってる程度でした。

jobcard.work

4.ジョブカードを作成

ジョブカードに必要な書類を準備します。

  • キャリア・プランシート
  • 職務経歴シート
  • 職業能力証明(免許・資格)シート
  • 職業能力証明(学習歴・訓練歴)シート

ハローワークで頂いた「ジョブ・カード活用ガイド」にも入っていますが、以下のサイトの「全様式」から、全様式をまとめてダウンロードすることができます。

jobcard.mhlw.go.jp

5.訓練前キャリアコンサルティング

3で予約した日時に、4で作成したファイルを印刷して持参して、キャリアコンサルティングを受けるためにハローワークに訪問します。

キャリアコンサルティングは時間通り1時間で終了しました。 必要に応じて修正などが入るようですが、私の場合は特に修正はなく、キャリアコンサルタントから指定した口座がキャリアプランに適しているというお墨付きを頂き終了です。

6.教育訓練給付金受給資格確認の手続き

受講開始の1か月前までに教育訓練給付金受給資格確認の手続きを行う必要があります。 今回は受講開始まで6か月くらいありますが、ハローワークで訓練前キャリアコンサルティングを行ったついでに、そのまま担当窓口に行き給付の申請手続きを行いました。

必要書類

上記で申請は完了です。教育訓練給付の受給資格者のしおりを頂けました。

あとは、講座開始から6か月ごとに再度ハローワークに訪問して、申告を行うことになります。

実際の申請につきましては、場所や時期などで異なることもあるかと思いますので、詳しくは以下をご覧ください。

厚生労働省 教育訓練給付制度

Q&A~専門実践教育訓練給付金~

Azure Container Apps と Dapr と Go のサンプル

Azure Container Apps で動かす Goアプリから Dapr 経由でHTTPとかSMTPのサービスを使ってみます。合わせて、よく使いそうなSendGridでメール送信、Cronで定期実行も試したので記載。

まずは単純にnet/httpパッケージで、サンプルコードを実行するHTTPサーバーを実装。

type service struct {
    client dapr.Client
}
func main() {
    // Dapr
    cli, err := dapr.NewClient()
    if err != nil {
        panic(err)
    }
    defer cli.Close()

    svc := &service{
        client: cli,
    }

    http.HandleFunc("/cron", cron)
    http.HandleFunc("/smtp", svc.Smtp)
    http.HandleFunc("/sendgrid", svc.SendGrid)
    http.HandleFunc("/http", svc.Http)
    http.ListenAndServe(":8080", nil)
}

仕様を参考にContainer AppsでDaprを呼ぶコードを実装。コンポーネントの設定からContainer Appsで不要な schema を削除していきます。

Example

HTTP, SMTP, SendGridのOutput Bindingは、リクエスト内容は異なりますが、dapr.NewClient() で作ったclientでリクエストを送るだけですので基本的には同じです。

HTTP

HTTP binding spec

- name: http
  type: bindings.http
  version: v1
  metadata:
    - name: url
      value: http://example.com
func (svc *service) Http(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    in := &dapr.InvokeBindingRequest{
        Name:      "http",
        Operation: "get",
        Data:      []byte("\"Hello, HTTP.\""),
        Metadata:  map[string]string{"path": "/"},
    }

    out, err := svc.client.InvokeBinding(ctx, in)
    if err != nil {
        fmt.Fprint(w, err.Error())
        return
    }
    data := string(out.Data)
    log.Print("Hello, HTTP.")
    fmt.Fprint(w, data)
}
SMTP

SMTP binding spec

- name: smtp
  type: bindings.smtp
  version: v1
  metadata:
    - name: host
      value: "example.com"
    - name: port
      value: "587"
    - name: user
      value: "USER"
    - name: password
      value: "PASSWORD"
    - name: skipTLSVerify
      value: true
    - name: emailFrom
      value: "test@example.com"
    - name: emailTo
      value: "test@example.com"
    - name: subject
      value: "Example Smtp"
func (svc *service) Smtp(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    in := &dapr.InvokeBindingRequest{
        Name:      "smtp",
        Operation: "create",
        Data:      []byte("\"Hello, SMTP.\""),
        Metadata:  map[string]string{"subject": "SMTP"},
    }

    _, err := svc.client.InvokeBinding(ctx, in)
    if err != nil {
        fmt.Fprint(w, err.Error())
        return
    }
    log.Print("Hello, SMTP.")
    fmt.Fprint(w, "OK. SMTP.")
}
Twilio SendGrid

Twilio SendGrid binding spec

- name: sendgrid
  type: bindings.twilio.sendgrid
  version: v1
  metadata:
    - name: emailFrom
      value: "test@example.com"
    - name: emailTo
      value: "test@example.com"
    - name: subject
      value: "Example SendGrid"
    - name: apiKey
      value: "APIKEY"
func (svc *service) SendGrid(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    in := &dapr.InvokeBindingRequest{
        Name:      "sendgrid",
        Operation: "create",
        Data:      []byte("\"Hello, SendGrid.\""),
        Metadata:  map[string]string{"subject": "SendGrid"},
    }

    _, err := svc.client.InvokeBinding(ctx, in)
    if err != nil {
        fmt.Fprint(w, err.Error())
        return
    }
    log.Print("Hello, SendGrid.")
    fmt.Fprint(w, "OK. SendGrid.")
}
Cron

Cron binding spec

cronはコンポーネントの名前に設定したエンドポイントに、metadataに設定されたタイミングで HTTP POST リクエストが来ます。サンプルはログに「Hello, Cron.」と出力していますので、Log Analyticsの設定をしてログで確認。

- name: cron
  type: bindings.cron
  version: v1
  metadata:
    - name: schedule
      value: "@every 10m"
func cron(w http.ResponseWriter, _ *http.Request) {
    log.Print("Hello, Cron.")
    fmt.Fprint(w, "OK. Cron.")
}

f:id:taka1923:20211218095043p:plain

デプロイ

ACRなどを利用してAZコマンドで試すときは、 components.yaml などに上記のコンポーネント設定をまとめて1ファイルで記載し、az containerapp create の dapr-components で、そのファイルを指定。

コマンド例

az containerapp create `
  --name $NAME `
  --resource-group $RESOURCE_GROUP `
  --environment $CONTAINERAPPS_ENVIRONMENT `
  --image $IMAGE `
  --target-port 8080 `
  --ingress 'external' `
  --min-replicas 1 `
  --max-replicas 1 `
  --enable-dapr `
  --dapr-app-port 8080 `
  --dapr-app-id $ID `
  --dapr-components "./components.yaml" `
  --registry-login-server $LOG_SERVER `
  --registry-username $USERNAME `
  --registry-password $PASSWORD 

無事デプロイ完了。ポータルでも確認できました。

f:id:taka1923:20211218095116p:plain

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

Go の gRPC サーバーで Azure AD B2C の JWT 使ってみる

Go で gRPC サーバーを実装して、Azure AD B2Cで認証を試してみます。チュートリアルを参考に Azure AD B2C を準備します。

  1. チュートリアル:Azure Active Directory B2C テナントの作成
  2. チュートリアル:Azure Active Directory B2C に Web アプリケーションを登録する
  3. チュートリアル: Azure Active Directory B2C でユーザー フローとカスタム ポリシーを作成する

実装です。gRPCサーバー作成時に認証のInterceptor設定します。

   server := grpc.NewServer(grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(authenticate)))

認証処理でJWTトークンを取得。JWTの処理は jwt-go を使用しています。

func authenticate(ctx context.Context) (context.Context, error) {
    bearer, err := grpc_auth.AuthFromMD(ctx, "bearer")
    if err != nil {
        return nil, err
    }

    token, err := jwt.Parse(bearer, getKey)
    if err != nil {
        return nil, err
    }

    claims := token.Claims.(jwt.MapClaims)
    for key, val := range claims {
        fmt.Printf("%s\t%v\n", key, val)
    }

    return ctx, nil
}

jwkの処理は jwx を使って対応します。urlの「TENANT-NAME」と「POLICY-NAME」は適切な値に修正が必要です。

func getKey(token *jwt.Token) (interface{}, error) {
    url := "https://TENANT-NAME.b2clogin.com/TENANT-NAME.onmicrosoft.com/POLICY-NAME/discovery/v2.0/keys"
    set, err := jwk.Fetch(context.Background(), url)
    if err != nil {
        return nil, err
    }

    keyID, ok := token.Header["kid"].(string)
    if !ok {
        return nil, errors.New("kid が Header にありません。")
    }
    key, ok := set.LookupKeyID(keyID)
    if ok {
        var rawKey interface{}
        if err := key.Raw(&rawKey); err != nil {
            return nil, err
        }
        return rawKey, nil
    }

    return nil, fmt.Errorf("key(%q) が見つかりません。", keyID)
}

実装が終わりましたので、grpcurlで実行してみます。トークンは、Azureポータルでユーザーフローを実行して https://jwt.ms で確認も可能です。ユーザー フローをテストする が参考になります。

grpcurl -plaintext  -H "Authorization: Bearer **トークン**" localhost:9000 example.EchoService.Test 

アクセスできました。

f:id:taka1923:20210907202651p:plain

Microsoft Graph チュートリアル を Go で試してみる

Microsoft Graph チュートリアル の、Microsoft Graph で .NET Core アプリを構築する と似たような感じの簡単なサンプルを Go で試してみます。

まだパブリックプレビューにはなっていませんが、Microsoft Authentication Library (MSAL) for Go がありますので、認証(デバイスコードフロー)はこちらを使用。予定表は REST API を使用します。

github.com

docs.microsoft.com

今回作成したサンプル(認証のキャッシュや mailbox settings 部分は省略)
Graph チュートリアル go sample

アプリ作成

Go コンソール アプリを作成。MSAL Go を使用するのでインストール。

go get -u github.com/AzureAD/microsoft-authentication-library-for-go/

ポータルでアプリを登録する

ポータルでアプリを登録するを参考にアプリを登録します。ページに記載されているようにアプリケーション (クライアント) ID の値を使用しますので、メモしておきます。

Azure AD 認証を追加する

環境変数に以下の値を設定します。 mailbox settingsは今回使いませんでしたので、SCOPESから削除しています。

APP_ID:アプリケーション (クライアント) ID
SCOPES:user.read,calendars.readwrite

main.go

appId := os.Getenv("APP_ID")
scopes := strings.Split(os.Getenv("SCOPES"), ",")

MSAL Go のサンプルを参考にデバイスコードフローを実装します。

graph.go

app, err := public.New(appId, public.WithAuthority("https://login.microsoftonline.com/common"))
if err != nil {
    panic(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
defer cancel()
devCode, err := app.AcquireTokenByDeviceCode(ctx, scopes)
if err != nil {
    panic(err)
}
fmt.Printf("Device Code is: %s\n", devCode.Result.Message)
result, err := devCode.AuthenticationResult(ctx)
if err != nil {
    panic(fmt.Sprintf("got error while waiting for user to input the device code: %s", err))
}

予定表ビューを取得する

Microsoft Graph をアプリケーションに組み込みます。このアプリケーションでは http.Client を使用して Microsoft Graph を呼び出します。

graph.go

t := time.Now().UTC()
t1 := t.AddDate(0, 0, int(t.Weekday())*-1)
t2 := t1.AddDate(0, 0, 7)
url := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/calendarview?startDateTime=%s&endDateTime=%s", t1.Format("2006-01-02T00:00:00Z"), t2.Format("2006-01-02T00:00:00Z"))
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+g.token)

resp, err := g.cli.Do(req)
if err != nil {
    return err
}

新しいイベントを作成する

予定表ビューの取得と同様に http.Client を使用して Microsoft Graph を呼び出します。

graph.go

event := models.Event{}
event.Subject = subject
event.Start.DateTime = start.Format("2006-01-02T15:04:05Z")
event.Start.TimeZone = "UTC"
event.End.DateTime = end.Format("2006-01-02T15:04:05Z")
event.End.TimeZone = "UTC"
event.Body.Content = content
event.Body.ContentType = "Text"
event.Attendees = []models.Attendee{}
j, _ := json.Marshal(event)
req, _ := http.NewRequest("POST", "https://graph.microsoft.com/v1.0/me/events", bytes.NewBuffer(j))
req.Header.Set("Authorization", "Bearer "+g.token)
req.Header.Set("Content-Type", "application/json")
resp, err := g.cli.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

終わり

これで、デバイスコードフローで認証し、予定表ビューの取得、イベントを作成する Go アプリ(Graph チュートリアル go sample)は完成です。

Microsoft Graph チュートリアル 的なものを Go で試すことができました。

f:id:taka1923:20210609190333p:plain

Azure Functions カスタム ハンドラー + Golang で Event Grid トリガーを試してみる

Azure Functions custom handler in Go に EventGridTrigger がなかったので試してみました。

カスタム ハンドラーの説明、その他のサンプルは以下にありますので、設定とコードのみざっくりと。

Event Grid トリガーでQueue Storage 出力の function.json です。

function.json

{
  "bindings":[
    {
      "type":"eventGridTrigger",
      "name":"eventGridEvent",
      "direction":"in"
    },
    {
      "name": "$return",
      "type": "queue",
      "direction": "out",
      "queueName": "test-output-node",
      "connection": "AzureWebJobsStorage"
    }
  ]
}
Name Trigger Input Output
eventGridTrigger Event Grid Event Grid Queue Storage

イベントを受信する部分は、GoCustomHandlers.goとちょっと変えて、ginを使ってみます。

main.go

package main

import (
    "encoding/json"
    "fmt"
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "os"
)

func main() {
    customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
    if !exists {
        customHandlerPort = "7071"
    }
    fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort)

    r := gin.Default()
    r.GET("/", home)
    r.POST("/EventGridTrigger", eventGridTriggerHandler)
    r.Run(":" + customHandlerPort)
}

func home(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, nil)
}

func  eventGridTriggerHandler(ctx *gin.Context) {
    var req EventMessage
    if err := ctx.ShouldBindJSON(&req); err != nil {
        log.Fatalf(err.Error())
        return
    }

    var me EventGridData
    if val1, ok := req.Data["eventGridEvent"]; ok {
        jsonb, err := json.Marshal(val1)
        if err != nil {
            log.Fatalf(err.Error())
            return
        }
        if err := json.Unmarshal(jsonb, &me); err != nil {
            log.Fatalf(err.Error())
            return
        }
    }

    returnValue := HTTPResponse {}
    returnValue.Outputs.Res.Body = "OK"
    returnValue.Outputs.Res.StatusCode = "200"
    returnValue.ReturnValue = me.Data

    ctx.JSON(http.StatusOK, returnValue)
}
type HTTPResponse struct {
    Outputs struct {
        Res struct {
            Body       string `json:"body"`
            StatusCode string `json:"statusCode"`
        }                   `json:"res"`
    }
    Logs        []string
    ReturnValue interface{}
}

type EventMessage struct {
    Data map[string]interface{}
    Metadata map[string]interface{}
}

type EventGridData struct {
    Id string                 `json:"id"`
    Topic string             `json:"topic"`
    Subject string          `json:"subject"`
    Data struct {
        UserId int64            `json:"userId"`
        Name string                 `json:"name"`
    }                        `json:"data"`
    EventType string          `json:"eventType"`
    EventTime string          `json:"eventTime"`
    MetadataVersion string  `json:"metadataVersion"`
    DataVersion string      `json:"dataVersion"`
}

あとは、Azure上に必要なリソース作成してデプロイ。適当なカスタムトピック(Azure Event Grid イベント スキーマ)を送信して動作しましたので、お試しは完了です。

func SendTopic(url string) error {
    key1 := "****"
    msg := [1]Topic{}

    uid, err := uuid.NewRandom()
    if err != nil {
        log.Fatalf(err.Error())
    }
    msg[0].Id = uid.String()
    msg[0].Subject = "app/sample/test1"
    msg[0].EventType = "inserted"
    n := time.Now()
    msg[0].EventTime = n.Format(time.RFC3339)
    msg[0].Data.UserId = 1
    msg[0].Data.Name = "Example"
    msg[0].DataVersion = "1.0"
    json, _ := json.Marshal(msg)

    req, _ := http.NewRequest("POST", url, bytes.NewBuffer(json))
    req.Header.Set("aeg-sas-key", key1)
    client := new(http.Client)
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer func() {
        if resp.Body != nil {
            resp.Body.Close()
        }
    }()

    contents, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(contents))
    fmt.Println(err)
    if err != nil {
        return err
    }
    return err
}