Azure と ASP.NET でメールフォームを作りました
PRです。最近は Go 中心のお仕事でしたが、今年は .NET に戻りつつありますのでメールフォーム的なものを .NET で作りました。 仕組みは簡単で以下の通りです。
- API でフォームの内容を受ける(Azure Functions)
- 受信内容を登録(Azure Cosmos DB)
- メール送信などの通知処理を実行(Azure Cosmos DB の Change Feed でトリガーされる Azure Functions)
入力項目、バリデーションや通知設定は SQL Database や Azure Table Storage に入れてあります。 また、API Management 使ったり、サービスエンドポイントで各サービスを接続したりして、構成的には以下のような感じで作りました。
- API Managementからマネージド ID で接続
- Cosmos DBのトリガーを使用
- サービスエンドポイントを使用して接続
Cosmos DB Change Feed や Azure Functions については、以下などを参考にさせていただきました。
そのほか、
- ドキュメントサイトを Blob と CDN
- 管理サイトを Web Apps と Azure B2C
- ソース管理やCI/CDは Azure DevOps
通知などは Teams と連携させて、工数やコストをあまりお金をかけない仕組みで動いています。
出来たのはこんな感じで普通のフォームです。
SWF ドキュメント に詳細は記載していますが、試しにフォーム作ってみたい方がいましたら、お声がけください。宣伝でした。
教育訓練給付制度の専門実践教育訓練給付金について
いつものAzure関連のネタとは関係ないですが、専門実践教育訓練給付金の申請手続きを行ったので、簡単なまとめです。
もともと、国も支援に力入れている分野ではあったと思うのですが、"リスキリング支援「5年で1兆円」"ということで、「リスキリング」とか「学び直し」など、最近の市場のテーマですね。
リスキリングとは「新しい職業に就くために、あるいは、今の職業で必要とされるスキルの大幅な変化に適応するために、必要なスキルを獲得する/させること」
専門実践教育訓練給付金については以下がわかりやすかったです。
今回手続きしました専門実践教育訓練を含む、教育訓練給付制度の指定教育訓練講座は以下のサイトで検索できます。
教育訓練給付制度 厚生労働大臣指定教育訓練講座 検索システム
さて、手続きです。
マイナンバーを記載するところもありますし、手続きや今後の支給申請ごとにマイナンバーカードを提示することで写真2枚の提出が省略できますので、持参する身分証明書はマイナンバーカードが便利です。そのため、私は身分証明書としてマイナンバーカードを使用しました。
1.居住所を管轄しているハローワークを確認
居住所を管轄しているハローワークを調べます。居住地が東京の場合、東京ハローワークで管轄している市区町村を確認できます。
また、電話で確認したところ、支給対象者支給要件の確認は身分証明書を持って窓口に行く必要があり、予約などは不要でした。
2.受給資格の確認
身分証明書を持って管轄のハローワクーに訪問し、支給要件の確認をします。
相談窓口に書類を提出して調べて頂き、「教育訓練給付金支給要件回答書(専門実践教育訓練)」を受け取ります。 資格要件を満たしていましたので、今後の流れについても説明していただけました。
今後の流れ
3.訓練前キャリアコンサルティングの予約
ジョブカード作成支援予約専用サイトでキャリアコンサルティングを予約です。私のときは、1週間くらい先から予約可能で、予約可能な直近2週間では3-4割埋まってる程度でした。
4.ジョブカードを作成
ジョブカードに必要な書類を準備します。
- キャリア・プランシート
- 職務経歴シート
- 職業能力証明(免許・資格)シート
- 職業能力証明(学習歴・訓練歴)シート
ハローワークで頂いた「ジョブ・カード活用ガイド」にも入っていますが、以下のサイトの「全様式」から、全様式をまとめてダウンロードすることができます。
5.訓練前キャリアコンサルティング
3で予約した日時に、4で作成したファイルを印刷して持参して、キャリアコンサルティングを受けるためにハローワークに訪問します。
キャリアコンサルティングは時間通り1時間で終了しました。 必要に応じて修正などが入るようですが、私の場合は特に修正はなく、キャリアコンサルタントから指定した口座がキャリアプランに適しているというお墨付きを頂き終了です。
6.教育訓練給付金受給資格確認の手続き
受講開始の1か月前までに教育訓練給付金受給資格確認の手続きを行う必要があります。 今回は受講開始まで6か月くらいありますが、ハローワークで訓練前キャリアコンサルティングを行ったついでに、そのまま担当窓口に行き給付の申請手続きを行いました。
必要書類
- 教育訓練給付金及び教育訓練支援給付金受給資格確認票
- ジョブカード
- マイナンバー
- 給付金受け取り用の金融機関の通帳またはキャッシュカード
上記で申請は完了です。教育訓練給付の受給資格者のしおりを頂けました。
あとは、講座開始から6か月ごとに再度ハローワークに訪問して、申告を行うことになります。
実際の申請につきましては、場所や時期などで異なることもあるかと思いますので、詳しくは以下をご覧ください。
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
- 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
- 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
- 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はコンポーネントの名前に設定したエンドポイントに、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.") }
デプロイ
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
無事デプロイ完了。ポータルでも確認できました。
Go で Azure App Configuration を使う
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に入ってくる値を良い感じに処理する必要がありそうですね。
Go の gRPC サーバーで Azure AD B2C の JWT 使ってみる
Go で gRPC サーバーを実装して、Azure AD B2Cで認証を試してみます。チュートリアルを参考に Azure AD B2C を準備します。
- チュートリアル:Azure Active Directory B2C テナントの作成
- チュートリアル:Azure Active Directory B2C に Web アプリケーションを登録する
- チュートリアル: 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
アクセスできました。
Microsoft Graph チュートリアル を Go で試してみる
Microsoft Graph チュートリアル の、Microsoft Graph で .NET Core アプリを構築する と似たような感じの簡単なサンプルを Go で試してみます。
まだパブリックプレビューにはなっていませんが、Microsoft Authentication Library (MSAL) for Go がありますので、認証(デバイスコードフロー)はこちらを使用。予定表は REST API を使用します。
今回作成したサンプル(認証のキャッシュや 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 で試すことができました。
Azure Functions カスタム ハンドラー + Golang で Event Grid トリガーを試してみる
Azure Functions custom handler in Go に EventGridTrigger がなかったので試してみました。
カスタム ハンドラーの説明、その他のサンプルは以下にありますので、設定とコードのみざっくりと。
- Azure Functions のカスタム ハンドラー
- GitHub - Azure-Samples/functions-custom-handlers: Sample code for Azure Functions custom handlers
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 }