0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Tạo Sidecar bằng Go để truy cập Microsoft Graph qua gRPC

Đăng vào 3 tuần trước

• 8 phút đọc

Giới thiệu

Microsoft Graph là cánh cửa để truy cập các dịch vụ của Microsoft 365. Nó cho phép bạn truy cập thông tin như:

  • Dữ liệu của người dùngnhóm.
  • Emaillịch trên Outlook.
  • Tệp trên OneDrive.
  • Tin nhắn trên Teams.

Để gọi Microsoft Graph, bạn không chỉ cần thực hiện một yêu cầu GET với http.Client. Bạn cần xác thực qua Microsoft Entra ID (trước đây là Azure Active Directory), lấy Access Token OAuth2, và sau đó sử dụng nó trong mỗi yêu cầu.

👉 Điều này có thể làm phức tạp ứng dụng chính, vì nó cần biết chi tiết về xác thực. Một giải pháp thanh lịch là sử dụng mô hình Sidecar: một dịch vụ phụ trợ, chạy bên cạnh ứng dụng, xử lý sự phức tạp này.


1. Sidecar là gì?

Giả sử bạn có một ứng dụng (trong Go, .NET, Python, v.v.) cần truy cập Graph.

Bạn có thể nhúng logic xác thực trong chính ứng dụng của mình. Nhưng nếu nhiều ứng dụng cần làm điều đó, bạn sẽ có mã trùng lặp ở mọi nơi.

Sidecar giải quyết vấn đề này:

  • Là một dịch vụ nhỏ tách biệt (trong trường hợp này là Go) chạy bên cạnh ứng dụng.
  • giao tiếp với Microsoft Entra ID để lấy token.
  • gọi đến Microsoft Graph.
  • Nó cung cấp một API đơn giản (qua gRPC) để ứng dụng chính chỉ cần yêu cầu:

"Cho tôi dữ liệu của người dùng X"
Và sidecar sẽ xử lý phần còn lại.

📌 Trong kiến trúc Kubernetes, sidecar chạy trong cùng một pod với ứng dụng chính.

Điều này có nghĩa là ứng dụng truy cập sidecar qua localhost, mà không phụ thuộc vào mạng bên ngoài.


2. Kiến trúc

📌 Các bước diễn ra như sau:

  1. Ứng dụng gọi GetUser trên sidecar.

  2. Sidecar kiểm tra xem đã có Access Token hợp lệ trong bộ nhớ cache chưa.

    • Nếu không có, nó sẽ yêu cầu một cái mới từ Microsoft Entra ID.
    • Sidecar gọi Microsoft Graph với token trong header.
    • Graph trả về dữ liệu người dùng.
    • Sidecar chuyển đổi thành UserResponse và trả về cho ứng dụng qua gRPC.

3. Định nghĩa hợp đồng gRPC

Trước khi viết mã Go, chúng ta cần định nghĩa hợp đồng giao tiếp.

Tạo tệp graph.proto:

proto Copy
syntax = "proto3";

package graph;

// Dịch vụ mà sidecar sẽ cung cấp
service GraphService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

// Yêu cầu: khách hàng có thể cung cấp user_id
// Nếu trống, sidecar truy vấn "me" (người dùng hiện tại)
message UserRequest {
  string user_id = 1;
}

// Phản hồi: dữ liệu cơ bản của người dùng trong Graph
message UserResponse {
  string id = 1;
  string display_name = 2;
  string given_name = 3;
  string surname = 4;
  string user_principal_name = 5;
}

Giải thích

  • Dịch vụ GraphService cung cấp phương thức GetUser.
  • UserRequest cho phép truy vấn một người dùng cụ thể (users/{id}) hoặc người dùng hiện tại (me).
  • UserResponse trả về một số trường thông thường từ Graph.

Tạo mã Go

Với protoc đã cài đặt, chạy:

bash Copy
protoc --go_out=. --go-grpc_out=. graph.proto

Điều này sẽ tạo ra các tệp .pb.go, chứa các giao diện gRPC mà chúng ta sẽ triển khai.


4. Triển khai sidecar trong Go

4.1 Cấu trúc cơ bản

Tạo tệp sidecar.go:

go Copy
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net"
    "net/http"
    "os"
    "sync"
    "time"

    pb "example.com/graph/proto" // điều chỉnh cho đúng đường dẫn
    "google.golang.org/grpc"
)

const (
    tokenURL = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
    graphURL = "https://graph.microsoft.com/v1.0/%s"
)

// Máy chủ gRPC sẽ chạy như một sidecar
type server struct {
    pb.UnimplementedGraphServiceServer
    mu          sync.Mutex
    accessToken string
    expiration  time.Time
}

📌 Ở đây, chúng ta đã định nghĩa:

  • Các hằng số với các endpoint của xác thực (Entra ID) và Graph.
  • Một struct server mà:
    • Triển khai dịch vụ gRPC của chúng ta.
    • Lưu trữ trong bộ nhớ (accessToken, expiration) token cuối cùng đã lấy.

4.2 Quản lý Token

go Copy
func (s *server) getToken(ctx context.Context) (string, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    // Nếu đã có token hợp lệ, tái sử dụng
    if time.Now().Before(s.expiration) && s.accessToken != "" {
        return s.accessToken, nil
    }

    // Nếu không, yêu cầu token mới từ Entra ID
    tenantID := os.Getenv("ENTRA_TENANT_ID")
    clientID := os.Getenv("ENTRA_CLIENT_ID")
    clientSecret := os.Getenv("ENTRA_CLIENT_SECRET")

    url := fmt.Sprintf(tokenURL, tenantID)
    data := []byte(fmt.Sprintf(
        "client_id=%s&scope=https%%3A%%2F%%2Fgraph.microsoft.com%%2F.default&client_secret=%s&grant_type=client_credentials",
        clientID, clientSecret,
    ))

    req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("lỗi khi lấy token: %s", string(body))
    }

    var token struct {
        AccessToken string `json:"access_token"`
        ExpiresIn   int    `json:"expires_in"`
    }
    if err := json.Unmarshal(body, &token); err != nil {
        return "", err
    }

    // Cập nhật cache (tái tạo 1 phút trước khi hết hạn)
    s.accessToken = token.AccessToken
    s.expiration = time.Now().Add(time.Duration(token.ExpiresIn-60) * time.Second)

    return s.accessToken, nil
}

Giải thích

  • Bộ nhớ cache token: sidecar không yêu cầu token mỗi lần → tiết kiệm yêu cầu.
  • Mutex (mu): đảm bảo rằng nhiều cuộc gọi đồng thời không thực hiện POST cùng một lúc.
  • Gia hạn trước: 1 phút trước khi hết hạn, sidecar yêu cầu mới.

4.3 Triển khai GetUser

go Copy
func (s *server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
    // 1. Đảm bảo token hợp lệ
    token, err := s.getToken(ctx)
    if err != nil {
        return nil, err
    }

    // 2. Tạo endpoint
    userEndpoint := "me"
    if req.UserId != "" {
        userEndpoint = "users/" + req.UserId
    }

    url := fmt.Sprintf(graphURL, userEndpoint)
    httpReq, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    httpReq.Header.Set("Authorization", "Bearer "+token)

    // 3. Thực hiện yêu cầu tới Microsoft Graph
    resp, err := http.DefaultClient.Do(httpReq)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("lỗi khi tìm người dùng: %s", string(body))
    }

    // 4. Ánh xạ phản hồi sang UserResponse
    var user map[string]interface{}
    if err := json.Unmarshal(body, &user); err != nil {
        return nil, err
    }

    return &pb.UserResponse{
        Id:                user["id"].(string),
        DisplayName:       user["displayName"].(string),
        GivenName:         user["givenName"].(string),
        Surname:           user["surname"].(string),
        UserPrincipalName: user["userPrincipalName"].(string),
    }, nil
}

Giải thích

  1. Lấy token từ bộ nhớ cache (hoặc làm mới).
  2. Xác định endpoint (/me hoặc /users/{id}).
  3. Thực hiện GET trên Microsoft Graph.
  4. Chuyển đổi phản hồi JSON sang struct UserResponse.

4.4 Khởi động máy chủ gRPC

go Copy
func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        panic(err)
    }
    grpcServer := grpc.NewServer()
    pb.RegisterGraphServiceServer(grpcServer, &server{})
    fmt.Println("Sidecar Microsoft Graph đang chạy trên :50051")
    if err := grpcServer.Serve(lis); err != nil {
        panic(err)
    }
}

Giải thích

  • Sidecar lắng nghe trên cổng 50051.
  • Nó đăng ký dịch vụ GraphService.
  • Nó vẫn chạy, sẵn sàng đáp ứng các cuộc gọi gRPC từ ứng dụng chính.

5. Thực hiện

  1. Cấu hình biến môi trường:
bash Copy
   export ENTRA_CLIENT_ID="client-id-của-bạn"
   export ENTRA_CLIENT_SECRET="secret-của-bạn"
   export ENTRA_TENANT_ID="tenant-id-của-bạn"
  1. Chạy sidecar:
bash Copy
   go run sidecar.go
  1. Kiểm tra với grpcurl:
bash Copy
   grpcurl -plaintext -d '{}' localhost:50051 graph.GraphService/GetUser

6. Ví dụ phản hồi

json Copy
{
  "id": "1234abcd-...",
  "display_name": "Nguyễn Văn A",
  "given_name": "Nguyễn",
  "surname": "Văn A",
  "user_principal_name": "nguyenvana@doanhnghiep.com"
}

7. Tại sao nên sử dụng Sidecar?

  • Đơn giản: ứng dụng chỉ cần gọi gRPC, không cần lo lắng về OAuth2.
  • Tái sử dụng: nhiều dịch vụ có thể chia sẻ cùng một sidecar.
  • Bảo mật: thông tin xác thực chỉ nằm trong sidecar.
  • Mở rộng: sidecar có thể được sao chép trong các pod khác nhau.
  • Theo dõi: số liệu và nhật ký có thể được tập trung trong sidecar.

Kết luận

Chúng ta đã tạo ra một sidecar bằng Go mà:

  1. Xác thực trên Microsoft Entra ID qua Client Credentials Flow.
  2. Sử dụng Microsoft Graph để truy vấn người dùng.
  3. Cung cấp một dịch vụ gRPC đơn giản cho ứng dụng chính.

Với điều này, chúng ta đã đạt được một thiết kế sạch hơn, an toàn hơn và dễ mở rộng, tách biệt trách nhiệm và tận dụng lợi ích của mô hình Sidecar.


💡 Bạn thấy hay?

Nếu muốn trao đổi về AI, cloud và kiến trúc, hãy theo dõi tôi trên mạng xã hội:

  • Các Mạng Xã Hội

Tôi thường xuyên đăng các nội dung kỹ thuật từ thực tiễn. Và khi khám phá một công cụ giúp tiết kiệm thời gian và mang lại hiệu quả, bạn sẽ được biết ngay lập tức.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào