Workload Identity連携でAWSからGoogle Cloudにセキュアにアクセスする

この記事は個人ブログと同じ内容です


はじめに

GoogleCloudのWorkload Identityでの連携をAWSのLambdaを使用し、S3のファイルをGoogle CloudのCloud Storageにアップロードする方法を解説します。本記事では、Workload Identityを利用して、IAMロールとGoogle Cloudのサービスアカウントを紐付けることで、安全にアクセスできる環境を構築します。

lambdaではPythonを使用します。また、Terraformを使用します。

Workload Identityとは

Workload Identityとは、AWSや他のクラウドプロバイダーのIAMロールとGoogle Cloudのサービスアカウントを紐付けることで、シークレットキーを使わずにGoogle Cloudのリソースにアクセスできる仕組みです。

Workload Identity Poolとは

Workload Identity Poolは、外部のクラウドプロバイダーやIDプロバイダーからの認証情報を受け入れるためのリソースです。

Workload Identity Providerとは

Workload Identity Providerは、特定のクラウドプロバイダーのIDをWorkload Identity Poolに関連付けるためのリソースです。

処理の流れ

  1. LambdaがS3のファイルを取得する。
  2. Lambdaに紐づくIAMロールがWorkload IdentityによってGoogle Cloudのサービスアカウントの権限を借用する。
  3. LambdaがCloud Storageにファイルをアップロードする。

Workload Identityでは、AWSのIAMロールがGoogle Cloudのサービスアカウントの権限を借用することで、Google Cloudのサービスにアクセスできるようになります。

Terraformで構築していく

IAMロールとS3バケットの作成

LambdaにアタッチするIAMロールとS3バケットを作成します。 Lambdaの構築にはGoogle Cloudの設定が必要となるため、後で行います。

# IAM Role の作成
resource "aws_iam_role" "lambda_role" {
  name = "lambda_execution_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

# IAMポリシー(S3読み取り権限)
resource "aws_iam_policy" "s3_read_policy" {
  name        = "s3_read_policy"
  description = "Allow Lambda to read from S3"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action   = ["s3:GetObject", "s3:ListBucket"]
      Effect   = "Allow"
      Resource = [
        aws_s3_bucket.lambda_bucket.arn,
        "${aws_s3_bucket.lambda_bucket.arn}/*"
      ]
    }]
  })
}

# IAMポリシーをIAMロールにアタッチ
resource "aws_iam_role_policy_attachment" "lambda_s3_read" {
  policy_arn = aws_iam_policy.s3_read_policy.arn
  role       = aws_iam_role.lambda_role.name
}

# Lambda 用のS3バケット作成
resource "aws_s3_bucket" "lambda_bucket" {
  bucket = "test-bucket"
}

サービスアカウントとCloud Storageの作成

Google Cloud上でサービスアカウントとCloud Storageを作成し、サービスアカウントにCloud Storageへのアクセス権を付与します。

resource "google_service_account" "main" {
  account_id   = "gc-test-sa"
  display_name = "gc-test-sa"
}

resource "google_storage_bucket" "main" {
  name                     = "gc-test-storage"
  location                 = "ASIA-NORTHEAST1"
  force_destroy            = true
  public_access_prevention = "enforced"
  storage_class            = "REGIONAL"

}

resource "google_storage_bucket_iam_member" "main_viewer" {
  bucket = google_storage_bucket.receiver.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.main.email}"
}

resource "google_storage_bucket_iam_member" "main_creator" {
  bucket = google_storage_bucket.receiver.name
  role   = "roles/storage.objectCreator"
  member = "serviceAccount:${google_service_account.main.email}"
}

Workload Identityの作成

Workload IdentityプールとWorkload Identityプロバイダを作成します。

resource "google_iam_workload_identity_pool" "aws_pool" {
  project                   = local.project_id
  workload_identity_pool_id = "test-aws-pool"
  display_name              = "test-aws-pool"
  description               = "aws pool for test"
}

resource "google_iam_workload_identity_pool_provider" "aws_provider" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "test-aws-provider"
  display_name                       = "test-aws-provider"
  description                        = "test-aws provider"
  aws {
    account_id = local.aws_id #AWSアカウントのID
  }
}

Workload IdentityでサービスアカウントとIAMロールの紐付け

Workload IdentityでサービスアカウントとIAMロールの紐付けをします。 principalSetの設定を書くときには、以下のような形である必要があります。

principalSet://iam.googleapis.com/${WorkloadIdentity Poolの名前}/attribute.aws_role/arn:aws:sts::${AWSのアカウントID}:assumed-role/${IAMロール名}

以下の部分はIAMロールのARNではないことに注意してください。

arn:aws:sts::${AWSのアカウントID}:assumed-role/${IAMロール名}

resource "google_service_account_iam_binding" "aws" {
  service_account_id = google_service_account.main.id
  role               = "roles/iam.workloadIdentityUser"

  members = [    "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.aws_pool.name}/attribute.aws_role/arn:aws:sts::${local.aws_id}:assumed-role/${local.aws_role_name}"
  ]
}

構成ファイルのダウンロード

以下の記事を参考に、Google Cloudのコンソール、またはgcloudのコマンドからダウンロードしてください。

https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds?hl=ja#console_3

ワークスペース間をまたぐと、構成ファイルのダウンロードはGoogle Cloudのコンソールではできなくて、gcloudのコマンドを使用してダウンロードする必要がある。

Lambdaの作成

フォルダー構成は以下のようになります。

root
  ┣━ lambda
  ┃     ┗━ src
  ┃        ┣━ test.py
  ┃        ┣━ GoogleCloudの構成ファイル.json
  ┃        ┗━ lambda.zip          
  ┣━ main.tf
  ┣━ variables.tf

今回はPythonで実装しています。 lambda/src/test.pyソースコードとなり、archive_file リソースが自動でZIP化してくれます。

環境変数 GOOGLE_APPLICATION_CREDENTIALSGoogle Cloud の構成ファイルのパスを設定するのを忘れないでください。

# `test.py` をZIP化
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "lambda/src" # ローカルの `lambda/src` をZIP化
  output_path = "lambda/src/lambda.zip"
}

# Lambda関数の作成
resource "aws_lambda_function" "my_lambda" {
  function_name    = "MyPythonLambda"
  role            = aws_iam_role.lambda_role.arn
  runtime         = "python3.9"
  handler         = "test.lambda_handler"
  filename        = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  environment {
    variables = {
      GOOGLE_APPLICATION_CREDENTIALS = "構成ファイルのパス/構成ファイル名"
    }
  }
}

test.py

import boto3
from google.cloud import storage

def get_from_s3(s3_bucket_name, s3_object_name):
    # S3クライアントの作成
    s3 = boto3.client('s3')
    s3_object_path = f"{s3_bucket_name}/{s3_object_name}"
    tmp_file_path = f"/tmp/{s3_object_name}"
    
    # ファイルを Lambda の一時領域にダウンロード
    s3.download_file(s3_bucket_name, s3_object_name, tmp_file_path)
    print(f"{s3_object_path} was downloaded to {tmp_file_path}.")
    
    return tmp_file_path

def upload_to_gcs(tmp_file_path, gcs_bucket_name):
    # Cloud Storage クライアントの作成
    gcs = storage.Client()
    file_name = tmp_file_path.split('/')[-1]
    gcs_object_path = f"my_gcs_path/{file_name}"
    
    bucket = gcs.bucket(gcs_bucket_name)
    blob = bucket.blob(gcs_object_path)
    
    # オブジェクトを Cloud Storage バケットにアップロード
    blob.upload_from_filename(tmp_file_path)
    print(f"{tmp_file_path} was uploaded to {gcs_bucket_name}/{gcs_object_path}.")
    
    return None

def lambda_handler(event, context):
    # event から各種情報を取得
    s3_bucket_name = event['s3_bucket_name']
    s3_object_name = event['s3_object_name']
    gcs_bucket_name = event['gcs_bucket_name']
    
    # オブジェクトを S3 から取得
    tmp_file_path = get_from_s3(
        s3_bucket_name=s3_bucket_name,
        s3_object_name=s3_object_name
    )
    
    # オブジェクトを Cloud Storage にアップロード
    upload_to_gcs(
        tmp_file_path=tmp_file_path,
        gcs_bucket_name=gcs_bucket_name
    )
    
    return {'statusCode': 200}

注意点

  • Workload Identity を利用して AWS IAM ロールと Google Cloud のサービスアカウントを紐付ける際、プリンシパルセットの設定が必要です。その際、AWS の IAM ロールの ARN はそのまま使用できないため、適切な形式に変換する必要があります。
  • Lambda の環境変数GOOGLE_APPLICATION_CREDENTIALS を設定する必要があります。
  • ワークスペースをまたぐ場合、構成ファイルのダウンロードは Google Cloud コンソールでは行えません。gcloud コマンドを使用してダウンロードする必要があります。

感想

Google Cloudが初めてということもあり、概念の理解にかなり苦労しました。 今まではサービスアカウントでキーを発行する方法でやっていたのですが、今回のWorkload Identiytによる認証でよりセキュアにすることができました。 不明点や間違い、感想ありましたらコメントいただけると幸いです。

参考記事

https://laboratory.kiyono-co.jp/1638/gcp/

https://zenn.dev/nextbeat/articles/google-workload-identity

https://blog.g-gen.co.jp/entry/using-workload-identity-federation-with-aws

TerraformでSecurity Command CenterのアラートをSlackに通知する

この記事は個人ブログと同じ内容です


Security Command Center(SCC)とは

Google CloudのSecurity Command Center(SCC)は、クラウド環境のセキュリティリスクの可視化・検出・管理を行うためのセキュリティ管理ツールです。

主な機能:

• 脆弱性の検出(IAMの設定ミス、不正アクセス、脆弱なVMなど)
• 脅威の監視(マルウェアや異常なネットワークトラフィックの検出)
• コンプライアンス管理(PCI DSS、ISO 27001 などの基準への適合確認)

Google Cloud環境全体のセキュリティを統合的に管理できるサービスです。

今回作るもの

以下のスクショのように、SCCでステータスがアクティブなアラートを検出したらSlackに通知するBotを作成します。 アラートの重要度で色分けされるようになっています。 Terraformで作成していきます。

全体の流れ

流れとしては以下の通りです。

SCC -> NotificationConfig -> Pub/Sub -> CloudRunFunctions -> Slack

GCにはAWS Chatbotのようなものがないため、CloudRunFunctionsで自前でコードを書く必要があります。 CloudRunFunctionsはGoで書いています。

実際に作っていく

GCプロジェクトは作成済み前提で進めていきます。

Slack APIでアプリケーションの作成

https://api.slack.com/apps

以下の記事を参考にしながら、上記よりSlackアプリケーションを作成し、OAuth Tokenを取得し、 Slackチャンネルにアプリケーションを追加してください。

必要なもの

  • OAuth Token
  • SlackのチャンネルID

https://qiita.com/kobayashi_ryo/items/a194e620b49edad27364

NotificationConfigとPub/Subの作成

SCCのリソースは存在しないので、google_scc_v2_project_notification_configでNotificationConfigを作り、Pub/SubとSCCを紐づける形になります。 このとき、streaming_configでSCCから通知したいアラートの条件をフィルタリングできます。

resource "google_scc_v2_project_notification_config" "main" {
  config_id    = "scc-slack-notification-config"
  description  = "Security Command Center Finding Notification Configuration"
  pubsub_topic = google_pubsub_topic.main.id

  streaming_config {
    # ステータスがアクティブでミュートされていないものを通知する
    filter = "NOT mute=\"MUTED\" AND state=\"ACTIVE\""
  }
}
resource "google_pubsub_topic" "main" {
  name = "scc-slack-notification-topic"
}

CloudRunFunctionsの作成

今回はCloudRunFunctionsにコードをデプロイするために、ソースコードのZipファイルをCloud Storageにアップロードし、それを元にCloudRunFunctionsを構築するようにしました。 ファイル構成は以下の様になります。

root
  ┣━ code
  ┃     ┗━ main.go
  ┃     ┗━ go.mod
  ┃     ┗━ go.sum
  ┃     ┗━ main.zip # 自動で生成される       
  ┣━ main.tf
  ┣━ variables.tf

Terraformリソース

archive_fileリソースを使用することにより、ファイルのZip化を自動でしてくれます。

また、google_storage_bucket_objectリソースを使用することで、ソースコードの変更を検知し、CloudRunFunctionsを手動で作り直す必要がなくなります。

google_cloudfunctions2_functionでPub/Subとの紐付けをしています。

data "archive_file" "main" {
  type        = "zip"
  source_dir  = "${path.module}/code"
  output_path = "${path.module}/code/main.zip"
}

resource "google_storage_bucket" "main" {
  name                     = "scc-slack-notification"
  location                 = "ASIA-NORTHEAST1"
  force_destroy            = true
  public_access_prevention = "enforced"
  storage_class            = "REGIONAL"
}

resource "google_storage_bucket_object" "main" {
  name   = "main.zip"
  bucket = google_storage_bucket.main.id
  source = data.archive_file.main.output_path
}

# dataを使用しないとソースコードの変更があったときに、cloud functionが更新されないので使用している
data "google_storage_bucket_object" "main" {
  bucket = google_storage_bucket_object.main.bucket
  name   = google_storage_bucket_object.main.name
}


resource "google_cloudfunctions2_function" "main" {
  name        = "scc-slack-notification-function"
  location    = "asia-northeast1"
  description = "Slack notification function"

  build_config {
    runtime     = "go121"
    entry_point = "NotifySlack"
    source {
      storage_source {
        bucket     = data.google_storage_bucket_object.main.bucket
        object     = data.google_storage_bucket_object.main.name
        generation = data.google_storage_bucket_object.main.generation
      }
    }
  }
  event_trigger {
    event_type   = "google.cloud.pubsub.topic.v1.messagePublished"
    pubsub_topic = google_pubsub_topic.main.id
    retry_policy = "RETRY_POLICY_DO_NOT_RETRY"
  }

  service_config {
    environment_variables = {
      SLACK_TOKEN          = var.slack_token
      SLACK_CHANNEL        = var.slack_channel
      GOOGLE_CLOUD_PROJECT = var.project_id
    }
    max_instance_count = 1
    available_memory   = "256M"
    timeout_seconds    = 60
  }

}

variableでslack_tokenとslack_channelを設定しているので、忘れずに設定してください。 slack_tokenは作成したSlackアプリケーションのOAuth Token、 slack_channelはアプリケーションを追加したSlackチャンネルのIDです。

variable "project_id" {
  description = "google cloudのプロジェクトID"
  type        = string
}

variable "slack_token" {
  type        = string
  description = "SlackのOAuthトークン"
  sensitive   = true
}

variable "slack_channel" {
  type        = string
  description = "SlackのチャンネルID"
  sensitive   = true
}

ソースコード

アラートの種類でSlackに通知する色が変わるようになっています。

cloud.google.com/go/loggingを使用することにより、CloudRunFunctionsのログの重要度を指定できる様になり、エラーログが発見しやすくなります。cloud.google.com/go/loggingを使用しないと、全ての重要度がinfoになり、エラーログの検索に時間がかかります。

Slackへの通知は、github.com/slack-go/slackを使用しています。 SlackAPIを叩く方法もあったのですが、より簡単に実装できるslack-goを選択しました。

SCCの内容をもっとSlackへのメッセージへ追加したい場合は、 GCのコンソールの「SCC」→「検出結果」へ行き、検出結果の詳細のモーダルのJSONタブを見ると、アラート内容のJSONが確認できるので、ここを元に内容を追加してください。

package code

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"

    "cloud.google.com/go/logging"
    "github.com/slack-go/slack"
)

var logger *logging.Logger

type SecurityResult struct {
    NotificationConfigName string `json:"notificationConfigName"`
    Finding                struct {
        Name          string `json:"name"`
        CanonicalName string `json:"canonicalName"`
        Parent        string `json:"parent"`
        ResourceName  string `json:"resourceName"`
        State         string `json:"state"`
        Category      string `json:"category"`
        Description   string `json:"description"`
        Severity      string `json:"severity"`
        EventTime     string `json:"eventTime"`
        CreateTime    string `json:"createTime"`
    } `json:"finding"`
    Resource struct {
        Name        string `json:"name"`
        Type        string `json:"type"`
        GcpMetadata struct {
            Project            string `json:"project"`
            ProjectDisplayName string `json:"projectDisplayName"`
        } `json:"gcpMetadata"`
    } `json:"resource"`
}

type PubsubMessage struct {
    Message struct {
        Data []byte `json:"data,omitempty"`
        ID   string `json:"id"`
    } `json:"message"`
    Subscription string `json:"subscription"`
}

func init() {
    // Google Cloud Loggingのクライアントを作成
    client, err := logging.NewClient(context.Background(), os.Getenv("GOOGLE_CLOUD_PROJECT"))
    if err != nil {
        log.Fatalf("Failed to create logging client: %v", err)
    }
    logger = client.Logger("security-logs")
}

// NotifySlack はHTTPリクエストを受け取り、Security Command Centerの検出結果をSlackに通知します
func NotifySlack(w http.ResponseWriter, r *http.Request) {
    if r.Body == nil {
        logError("リクエストボディが空です")
        return
    }

    // Pub/Subからのメッセージ構造
    var pubsubMessage PubsubMessage

    // リクエストボディをデコード
    if err := json.NewDecoder(r.Body).Decode(&pubsubMessage); err != nil {
        logError(fmt.Sprintf("JSONデコードエラー: %v", err))
        return
    }

    // Security Command Centerの検出結果をパース
    var result SecurityResult
    if err := json.Unmarshal(pubsubMessage.Message.Data, &result); err != nil {
        logError(fmt.Sprintf("Security Command Centerの検出結果のパースエラー: %v", err))
        return
    }

    // Slackに通知を送信
    if err := sendSlackNotification(result); err != nil {
        logError(fmt.Sprintf("Slack通知エラー: %v", err))
        return
    }

    logInfo(fmt.Sprintf("Security Command Centerの検出結果をSlackに通知しました: %s", result.Finding.Name))
}

// sendSlackNotification はSecurity Command Centerの検出結果をSlackに送信します
func sendSlackNotification(result SecurityResult) error {
    slackToken := os.Getenv("SLACK_TOKEN")
    if slackToken == "" {
        return fmt.Errorf("SLACK_TOKENが設定されていません")
    }
    slackChannel := os.Getenv("SLACK_CHANNEL")
    if slackChannel == "" {
        return fmt.Errorf("SLACK_CHANNELが設定されていません")
    }

    // Slackクライアントの初期化
    api := slack.New(slackToken)

    color := "#3AA3E3" // デフォルト色
    switch result.Finding.Severity {
    case "CRITICAL":
        color = "#FF0000" // 赤
    case "HIGH":
        color = "#FFA500" // オレンジ
    case "MEDIUM":
        color = "#FFFF00" // 黄色
    case "LOW":
        color = "#00FF00" // 緑
    }

    eventTime, err := time.Parse(time.RFC3339, result.Finding.EventTime)
    if err != nil {
        return err
    }
    // 日本時間(JST)に変換
    location, err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        return err
    }
    jstTime := eventTime.In(location)

    titleLink, err := generateSCCLink(result.Finding.Name, result.Finding.CanonicalName)
    if err != nil {
        return err
    }

    // Slackメッセージの添付ファイルを作成
    attachment := slack.Attachment{
        Color:     color,
        Title:     fmt.Sprintf("Security Finding: %s", result.Finding.Category),
        TitleLink: titleLink,
        Text: fmt.Sprintf("リソース: %s\n重要度: %s\n状態: %s\n説明:\n%s",
            result.Resource.Name, result.Finding.Severity, result.Finding.State, result.Finding.Description),
        MarkdownIn: []string{"text"},
        Fields: []slack.AttachmentField{
            {
                Title: "プロジェクト",
                Value: result.Resource.GcpMetadata.ProjectDisplayName,
                Short: true,
            },
            {
                Title: "リソースタイプ",
                Value: result.Resource.Type,
                Short: true,
            },
            {
                Title: "検出時間",
                Value: jstTime.Format("2006年01月02日 15:04:05"),
                Short: true,
            },
            {
                Title: "カテゴリ",
                Value: result.Finding.Category,
                Short: true,
            },
        },
        Footer: "GC Security Command Center",
    }

    // Slackメッセージを送信
    _, _, err = api.PostMessage(
        slackChannel,
        slack.MsgOptionAttachments(attachment),
        slack.MsgOptionAsUser(true),
    )
    return err
}

// Security Command Centerの検出結果の詳細のリンクを生成
func generateSCCLink(name string, canonicalName string) (string, error) {
    // URL エンコード
    escapedName := url.PathEscape(name)

    // canonicalName から ProjectID を抽出
    parts := strings.Split(canonicalName, "/")
    if len(parts) < 2 {
        return "", fmt.Errorf("Invalid canonicalName format: %s", canonicalName)
    }
    projectID := parts[1] // projects/{projectID}

    // namePath から sourceId と findingId を抽出
    nameParts := strings.Split(name, "/")
    if len(nameParts) < 8 {
        return "", fmt.Errorf("Invalid namePath format: %s", name)
    }
    sourceID := nameParts[3]                 // sources/{sourceID}
    findingID := nameParts[len(nameParts)-1] // findings/{findingID}

    // 完全な URL を組み立て
    link := fmt.Sprintf(
        "https://console.cloud.google.com/security/command-center/findingsv2;name=%s;?project=%s&finding=%s&sourceId=%s",
        escapedName, projectID, findingID, sourceID,
    )

    return link, nil
}

// Google Cloud LoggingにINFOログを出力
func logInfo(message string) {
    logger.Log(logging.Entry{Severity: logging.Info, Payload: message})
}

// Google Cloud LoggingにERRORログを出力
func logError(message string) {
    logger.Log(logging.Entry{Severity: logging.Error, Payload: message})
}
module modules/gc-scc-slack-notification/code

go 1.21

require (
    cloud.google.com/go/logging v1.13.0
    github.com/slack-go/slack v0.16.0
)

require (
    cloud.google.com/go v0.117.0 // indirect
    cloud.google.com/go/auth v0.13.0 // indirect
    cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
    cloud.google.com/go/compute/metadata v0.6.0 // indirect
    cloud.google.com/go/longrunning v0.6.2 // indirect
    github.com/felixge/httpsnoop v1.0.4 // indirect
    github.com/go-logr/logr v1.4.2 // indirect
    github.com/go-logr/stdr v1.2.2 // indirect
    github.com/google/s2a-go v0.1.8 // indirect
    github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
    github.com/googleapis/gax-go/v2 v2.14.0 // indirect
    github.com/gorilla/websocket v1.4.2 // indirect
    go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
    go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
    go.opentelemetry.io/otel v1.29.0 // indirect
    go.opentelemetry.io/otel/metric v1.29.0 // indirect
    go.opentelemetry.io/otel/trace v1.29.0 // indirect
    golang.org/x/crypto v0.31.0 // indirect
    golang.org/x/net v0.33.0 // indirect
    golang.org/x/oauth2 v0.24.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/sys v0.28.0 // indirect
    golang.org/x/text v0.21.0 // indirect
    golang.org/x/time v0.8.0 // indirect
    google.golang.org/api v0.214.0 // indirect
    google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
    google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
    google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
    google.golang.org/grpc v1.67.3 // indirect
    google.golang.org/protobuf v1.35.2 // indirect
)

学んだことと感想

  • CloudRunFunctionsは第1世代と第2世代がある。 terraformでgoogle_cloudfunctions_functionを使用すると第1世代になるので注意。 google_cloudfunctions2_functionが第2世代。 参考にする記事が古いと第1世代を使用していることがあるので注意。

https://qiita.com/AoTo0330/items/1977c1ae14381d274c0b

  • Terraformのarchive_fileリソースが自動でファイルをZipにするのは、すごい便利だと思った
  • CloudRunFunctionsのログ重要度を変更し見やすくするためには、わざわざライブラリ使わないといけないのが面倒だと思った。

参考記事

Tidy First? を読んでみた

この記事は個人ブログと同じ内容です


Tidy とは「整頓する」という意味。 コードを書く際に、いきなりシステムの振る舞いを変えるような大きな変更をするのではなく、まずは周辺を整えるような小さなリファクタリングから始めようというのが本書の大筋になります。

以前、著者ケント・ベックさんの XP 本を読んだことがあり、今回新刊が出たということで前情報なしに購入しましたが、冒頭から引き込まれたので、要旨をまとめてみました。

本書について

第I部 整頓

  • ガード節
    • if 文によるネストした条件分岐を早期リターンに置き換え、ネストを減らすことで可読性を向上させる
    • 使いすぎると逆に可読性が下がるので注意
  • デッドコード
    • 実行されないコードは削除する
  • 読む順番
    • ファイルの中のコードを読み手が遭遇したいと思う順番に並び替える
  • 凝集の順番
    • コードの順番を入れ替えて、関連する処理が隣接するようにする
  • 説明変数
    • 大きくてややこしい式の一部を、意図がわかる変数に抽出する
  • 説明定数
    • コード中に繰り返し現れる固定値を定数に抽出する
  • ヘルパーを抽出する
    • 他のコードとの相互作用が限られているコードを抽出、別メソッドとして切り出す
  • 冗長なコメントを削除する
    • コードに書いてあることをそのまま書いてあるようなコメントは削除する

第II部 管理術

プルリクエストを分ける

プルリクエストは以下の単位に分割することで、レビューのしやすさと速度を向上させることができます。

  • 整頓用のプルリクエス
  • 振る舞いを変更したプルリクエス

整頓は連鎖する

  • 散らかったデッドコードを取り除いたら、読む順番や凝集の順番への並べ替えの方法が見えてくるかもしれない
  • 凝集の順番にグループ化されたコードは、ヘルパーとして抽出可能かもしれない
  • ヘルパーとして抽出すると、ガード節や説明定数を取り入れたり、冗長なコメントを削除できるかもしれない
  • 冗長なコメントのノイズを取り除くことで、より良い読む順番が見えてくるかもしれない

整頓のコストを見極める

  • 1プルリクエストあたりの整頓の量が少ない場合
    • コードが煩雑になり、レビューのコストが高くなる
  • 1プルリクエストあたりの整頓の量が多い場合
    • 他のメンバーとのコンフリクトのリスクや、振る舞いを予期せず変更してしまうリスクが高くなる

どうすればレビューのコストを減らしつつ、整頓のコストを下げることができるか? 解決策として、チームに信頼と盤石な文化があるのであれば、整頓のプルリクエストはレビュー必須にしないことが提案されていました。 チームがそのレベルに達するまでは何ヶ月もかかるかもしれませんが、練習と実験を繰り返すことが大切だそうです。

整頓のタイミング

  • 整頓しない
    • 二度と変更しないコードの場合
  • 改めて整頓する
    • 整頓することですぐに見返りがあるわけではなく、大きなかたまりのコードの場合
    • 例) 古い API から新しい API へ大量の移行
      • すぐに影響のある部分だけを変更して、残りは改めて整頓する
      • 小出しに整頓できる
  • あとに整頓する
    • 同じ領域を直近再び変更する可能性がある場合
    • いま整頓する方が見返りが少ない場合
  • 先に整頓する
    • すぐに見返りがある場合
    • 何をどのように整頓すればいいかわかっている場合

第III部 理論

ソフトウェア設計とは

ソフトウェア設計とは何か? それは「要素を役立つように関係づけること」と定義されています。

要素

  • 細胞 → 臓器 → 生物
  • 原子 → 分子 → 物質
  • トークン → 式 → 関数 → オブジェクト/モジュール → システム

関係づける

  • 呼び出す
  • 公開する
  • 待ち受ける
  • 参照する (変数の値を取得する)

役立つように

  • 要素の作成と削除
  • 関係性の作成と削除
  • 関係性が生み出す利益の増加

最初の言葉で置き換えてみると

  • 「式」を「要素を作成する」ように「参照する」
  • 「関数」を「関係性を削除する」ように「呼び出す」
  • 「システム」を「関係性が生み出す利益が増加する」ように「公開する」

上記のようなことをするのがソフトウェア設計と言えそうです。

なぜ整頓するのか

ソフトウェアは利益を生み出すために作られるため、利益に直接関係する振る舞いを変更することが求められます。 振る舞いを変更し続けるために煩雑なコードは障害となるため、整頓が必要です。

  • 整頓せずに振る舞いを変更
  • 先に整頓して、あとに振る舞いを変更

上記の2択への向き合い方として以下の4つを軸に考えるべきとされています。

  • コスト
    • 整頓によってコストが小さくなるか?
    • cost(整頓) + cost(整頓のあとに振る舞いを変更) < cost(整頓せずに振る舞いを変更) の場合は、先に整頓する
  • 収益
    • 整頓によって収益が大きくなるか?
  • 結合
    • 整頓によってより少ない要素の変更になるか?
  • 凝集
    • 整頓によって変更が必要な要素が、より小さく、もっと集中した範囲になるか?

まとめ

普段生活していると、物がどこにあるかわからなくなったり、汚れが溜まったりしていきます。 その状態を放置していると、探すのにより時間がかかるようになったり、汚れがこびりついて掃除するのに苦労するようになります。 しかし現代の生活では、なかなかそれらを解消するための時間が取れないことが多いです。

整頓は文字通り清掃と似ているなと感じました。 こまめに掃除をしていれば、掃除や物を探すのにかかる時間が短くなり、清潔な環境を保つことができます。 整頓も同じで、こまめに整頓をしていれば、コードを読んだりレビューする時間が短くなり、コードの品質を保つことができます。

またコードとは、小説やドキュメントと同じように人間が読む文章であり、その文章が読みやすいかどうかは、コードの品質に大きく影響すると感じました。

本書は、開発業務に携わっている方にとってはすでに知っている内容がありそうだなと思いつつ、いつやるか?なぜやるか?という点について深く考えたことはなかったため、学びになる内容でした。 今回新刊が出たということで読んでみましたが、どうやら本書の続編も予定しているらしく、引き続きお世話になることになりそうです。 興味のある方はぜひ読んでみてください。

Go における Adapter パターン - インターフェースの適用と統一

この記事は個人ブログと同じ内容です

www.ritolab.com


ソフトウェア開発では、異なるインターフェースを持つコンポーネント同士を統一的に扱いたい場面があります。そのような場合に役立つのが Adapter(アダプター)パターン です。

本記事では、Adapter パターンの基本概念を理解しつつ、Go 言語での具体的な実装方法を紹介します。

Adapter パターン

Adapter パターンは、「あるインターフェースを、別のインターフェースに適用させる」ためのデザインパターンです。既存のコードを変更せずに、新しいインターフェースで利用できるようにするのが特徴です。

Adapter パターンを適用すると、既存のコードを変更せずに新しい規格で利用可能にしたり、異なるライブラリや API を統一できたり、古いシステムと新しいシステムをつないだりできます。

Adapter パターンのイメージ

Adapter パターンの考え方を、日常的な例に当てはめると「海外のコンセントと変換プラグ」がわかりやすいです。

  • 日本の電化製品(100V, Aタイプのプラグ)ヨーロッパのコンセント(220V, Cタイプのソケット) に挿したい。
  • 直接は使えないので 変換プラグ(アダプター) を使う。

このように、異なる規格を持つものを適用させるのが Adapter パターンの役割です。

Go における Adapter パターンの実装

先ほどイメージしたプラグを実装に落とし込んでみます。

1. 既存のインターフェース(日本の電化製品)

日本の電化製品は A タイプのプラグを使用し、日本の 100V のコンセントに接続することを前提としています。以下の実装では、日本の電化製品である JapaneseDevice 構造体が JapanPlug インターフェースを実装しており、InsertIntoJapanSocket() メソッドを持っています。つまりこの電化製品は、日本の規格で使われることを前提としている実装になっています。

package main

import "fmt"

// JapanPlug 日本の電化製品のインターフェース
type JapanPlug interface {
  InsertIntoJapanSocket()
}

// JapaneseDevice 日本の電化製品
type JapaneseDevice struct{}

func (d *JapaneseDevice) InsertIntoJapanSocket() {
  fmt.Println("コンセントに接続しました。")
}

2. 新しいインターフェース(ヨーロッパのコンセント)

一方でヨーロッパのコンセントは C タイプのプラグ を使用し、220V の電圧で動作します。以下のインターフェース EuropePlug は、ヨーロッパの電化製品が InsertIntoEuropeSocket() を実装することを前提としています。

// EuropePlug ヨーロッパの電化製品のインターフェース
type EuropePlug interface {
  InsertIntoEuropeSocket()
}

つまり、日本の電化製品(JapaneseDevice構造体)では、プラグの形状が違う(EuropePlug インターフェースを実装していない)ので、ヨーロッパのコンセントには接続できないわけですね。

3. アダプター(変換プラグ)

ここで変換プラグの出番です。以下に定義した構造体 PlugAdapterEuropePlug を実装しつつ、日本の電化製品 (JapaneseDevice) を内部で使用します。InsertIntoEuropeSocket() が呼ばれると、JapaneseDevice.InsertIntoJapanSocket() を内部で実行し、日本の電化製品がヨーロッパのコンセントで使えるようになります。

// PlugAdapter EuropePlug を JapanPlug に適合させる
type PlugAdapter struct {
  JapaneseDevice *JapaneseDevice
}

// InsertIntoEuropeSocket EuropePlug のインターフェースを満たす
func (pa *PlugAdapter) InsertIntoEuropeSocket() {
  fmt.Println("変換プラグを使用中...")
  pa.JapaneseDevice.InsertIntoJapanSocket()
}

4. 変換プラグを使って日本の電化製品をヨーロッパで使用する

この PlugAdapter を使用することで、日本の電化製品をヨーロッパのコンセントに適用させることができます。

func main() {
  japanDevice := &JapaneseDevice{}
  adapter := &PlugAdapter{JapaneseDevice: japanDevice}

  // ヨーロッパのコンセント(EuropePlug)のインターフェースで、日本の電化製品を使う
  adapter.InsertIntoEuropeSocket()
}

// 変換プラグを使用中...
// コンセントに接続しました。

このように、Adapter パターンを利用することで、本来互換性のないインターフェース同士を橋渡しし、異なる規格のものを統一的に扱えるようになります。まるで通訳が異なる言語を話す人々の間を取り持つように、Adapter は異なるインターフェースの間でデータのやり取りを可能にします。Go における Adapter パターンの実装では、特定のインターフェースに適合しない既存の構造体を、新しい環境で使えるように変換する仕組みを提供することで、コードの再利用性と柔軟性を向上させることができます。

Adapter パターンの実践例: ロギングライブラリの統一

Adapter パターンは、異なるインターフェースを統一するために非常に便利です。実際の開発では、異なるライブラリや外部 API を統一的に扱いたい状況があるかもしれません。例えば、Go には Logrus や Zap など複数のロギングライブラリがあり、それぞれ異なる API を持っています。

ロギングライブラリ メソッド名の違い 使い方の違い
Logrus Info(message string) 直接メソッドを呼び出す
Zap Log(level string, message string) ログレベルを引数で指定する

例: Logrus のコード

logrus := LogrusLogger{}
logrus.Info("アプリケーションが起動しました")
logrus.Error("エラーが発生しました")

例: Zap のコード

zap := ZapLogger{}
zap.Log("INFO", "アプリケーションが起動しました")
zap.Log("ERROR", "エラーが発生しました")

このままでは、アプリケーションで異なるロガーを利用する際に、コードの変更が必要になります。そこで、Adapter パターンを使って両者を統一し、共通のインターフェースで扱えるようにします。

1. 共通のインターフェースの定義

まず、すべてのロギングライブラリを統一する 共通のインターフェース Logger を定義します。

// Logger 共通のロギングインターフェース
type Logger interface {
    Info(message string)
    Error(message string)
}

2. 既存のロギングライブラリの実装

次に、異なるロギングライブラリを定義します。

package main

import "fmt"

// LogrusLogger ログライブラリ A の実装
type LogrusLogger struct{}

func (l *LogrusLogger) Info(message string) {
    fmt.Println("[Logrus INFO]:", message)
}

func (l *LogrusLogger) Error(message string) {
    fmt.Println("[Logrus ERROR]:", message)
}

// ZapLogger ログライブラリ B の実装
type ZapLogger struct{}

func (z *ZapLogger) Log(level string, message string) {
    fmt.Printf("[Zap %s]: %s\n", level, message)
}

3. Adapter の実装

各ロギングライブラリを Logger インターフェースに適合させる Adapter を実装します。

// LogrusAdapter Logrus を共通インターフェースに適用
type LogrusAdapter struct {
    logrus *LogrusLogger
}

func (l *LogrusAdapter) Info(message string) {
    l.logrus.Info(message)
}

func (l *LogrusAdapter) Error(message string) {
    l.logrus.Error(message)
}

// ZapAdapter Zap を共通インターフェースに適用
type ZapAdapter struct {
    zap *ZapLogger
}

func (z *ZapAdapter) Info(message string) {
    z.zap.Log("INFO", message)
}

func (z *ZapAdapter) Error(message string) {
    z.zap.Log("ERROR", message)
}

4. Adapter を利用する

Adapter を利用すると、異なるロギングライブラリを Logger インターフェースを通じて統一的に扱うことができます。

func main() {
    // 異なるログシステムを統一
    logrusAdapter := &LogrusAdapter{logrus: &LogrusLogger{}}
    zapAdapter := &ZapAdapter{zap: &ZapLogger{}}

    // 共通の Logger インターフェースを利用
    loggers := []Logger{logrusAdapter, zapAdapter}

    for _, logger := range loggers {
        logger.Info("アプリケーションが起動しました")
        logger.Error("エラーが発生しました")
    }
}

出力結果:

[Logrus INFO]: アプリケーションが起動しました
[Logrus ERROR]: エラーが発生しました
[Zap INFO]: アプリケーションが起動しました
[Zap ERROR]: エラーが発生しました

このように、Adapter パターンを使うことで 異なるロギングライブラリの API を統一し、アプリケーションのコードを変更せずに扱えるようになりました。

  • Adapter パターンを使うことで、異なるロギングライブラリの API を統一できた
  • Logger インターフェースを定義し、LogrusAdapter と ZapAdapter を実装することで、異なるライブラリを一貫した方法で扱えるようになった
  • Adapter パターンを適用することで、アプリケーションのコードを変更せずにロギングライブラリを差し替えたり、追加したりできる

まとめ

Adapter パターンの基本概念と Go における実装方法をご紹介しました。Adapter パターンを適用すると、互換性のないインターフェースを統一し、異なるシステムやライブラリを柔軟に扱うことが可能になります。実際の開発では、外部 API の統一やライブラリの置き換えなど、多くの場面で活用できます。

より柔軟でメンテナブルなコードを実現していきたいですね。

ALBで簡単にメンテナンスモードを実装する

この記事は個人ブログと同じ内容です


はじめに

サービス運用の中で、計画的なメンテナンスや障害対応時に「メンテナンスモード」を導入することは重要です。本記事では、AWSのアプリケーションロードバランサー(ALB)を活用して、簡単にメンテナンスモードを実装する方法を紹介します。

ALBのリスナールールとターゲットグループを活用し、柔軟かつ迅速にモード切替を実現する方法について解説します。


実装方法

1. メンテナンスモード用のリスナールールを追加

まず、メンテナンスモードを実現するためのルールをALBのリスナーに追加します。

設定内容

  • 条件: パスがすべて(*)に一致するリクエストを対象とする。
  • アクション: 固定レスポンスを返す。例えば、HTTP 503ステータスコードと「ただいまメンテナンス中です。」というメッセージ。

メンテナンスページの表示について

今回は固定レスポンスで返しますが、自分で用意したメンテナンスページがある場合は、「URLにリダイレクト」で自分のURLを設定すると、よりカスタマイズされたページを表示できます。


2. エンジニアや特定のユーザーがアクセスできるようにするリスナールールの追加

メンテナンスモード中に動作確認などをするために、特定の条件で通常と同じようにアクセスできるようにします。 今回は、Cookiemaintenance_mode=trueが含まれる場合に通常のサービスにリダイレクトします。

設定内容

  • 条件: HTTPヘッダー内のCookiemaintenance_mode=trueが含まれる場合に通常のサービスを提供する。
  • アクション: 通常のターゲットグループにリクエストをフォワードする。

補足: 上記例では固定レスポンスを使用していますが、実運用では通常のターゲットグループを設定することを推奨します。

優先度の設定

Cookieを条件とするルールの優先度は、メンテナンスモードのルールよりも高く設定する必要があります。これにより、特定条件を満たしたユーザーは通常どおりアクセス可能になります。


補足

*maintenance_mode=true*にしないとうまく動作しない理由

以下はGoogle検索した時ですが、Cookieはリクエストの中に基本複数あります。

設定をmaintenance_mode=trueにすると、Cookieがこれだけの時にリダイレクトするという意味になります。基本WebサイトやアプリのCookieはmaintenance_mode=true以外に複数あるので、他のがあるとうまくリダイレクトされません。 ですので設定の前後に*をつけて、Cookieが複数の時でも対応できるようにしないといけません。


Terraformを使った実装例

前提条件

  • ALBは既に構築済み。
  • メンテナンスモードの有効化は変数(例: var.maintenance_mode)で切り替える。

Terraformコード

メンテナンスモードの通常アクセス許可ルール

resource "aws_lb_listener_rule" "maintenance_forward" {
  count = var.maintenance_mode ? 1 : 0

  listener_arn = aws_alb_listener.https.arn
  priority     = 100
  condition {
    http_header {
      http_header_name = "Cookie"
      values           = ["*maintenance_mode=true*"]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_alb_target_group.ecs_service.arn
  }
}

メンテナンスモードの固定レスポンスルール

resource "aws_lb_listener_rule" "maintenance_fixed_response" {
  count = var.maintenance_mode ? 1 : 0

  listener_arn = aws_alb_listener.https.arn
  priority     = 200
  condition {
    path_pattern {
      values = ["*"]
    }
  }

  action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "ただいまメンテナンス中です。"
      status_code  = "503"
    }
  }
}

解説

  • 条件設定: var.maintenance_modeを切り替えることで、メンテナンスモードの有効化・無効化を簡単に行えます。
  • 優先度設定: 通常アクセス用ルールの優先度(priority = 100)を高く設定し、すべてのパス用のルール(priority = 200)よりも先に評価されるようにしています。

まとめ

ALBを活用したメンテナンスモードの実装は、短時間で設定でき、運用に柔軟性を持たせられる方法です。特定のユーザーに通常アクセスを許可する設定や、Terraformでの自動化を組み合わせることで、効率的かつ安全にメンテナンスを行うことが可能になります。

メンテナンスの頻度や用途に応じて、今回の方法を適宜カスタマイズしてみてください!

NotebookLMの実践レビュー

この記事は個人ブログと同じ内容です


NotebookLMとは

NotebookLM(Notebook Language Model)は、Googleが提供するAIアシスタントツールで、指定したソースに基づいて質問に回答したり、情報を整理する支援を行います。特に、セキュリティや業務関連の質問回答を効率化する場面で活用されています。

https://notebooklm.google.com/


使った経緯

社内で、セキュリティチェックシートへの回答が求められる際、効率化のためにNotebookLMを導入しました。これまでスプレッドシートに回答を手入力していた状況で、既存の情報ソースを活用してAIに代替させる試みを行いました。

情報ソースとしては十分な資料が揃っており、NotebookLMを通じて効率的な回答作成が可能かを検証しました。


よかったこと

1. ソースがない場合は「ない」と回答する正直さ

ソースが存在しない場合、AIは明確に「該当するソースがありません」と回答してくれます。曖昧な推測を避けられる点が安心できます。
例:


2. 簡単にAIを作ることができる

NotebookLMは、ソースを登録するだけで簡単に使い始められます。対応可能なソース形式も多岐にわたり、以下のような種類を取り扱えます:

例:

ただし、以下の制約があります:

  • ソースの登録は最大50個まで。
  • Youtubeなどのソースの情報は文字起こし形式で登録される。

3. 参照元のソースを明確に表示

回答に基づいた参照元ソースを明示してくれるため、信頼性の高い回答を得ることができます。
例:


使う上での注意点やコツ

1. 回答精度の低下を防ぐ方法

NotebookLMは、回答を繰り返すと前の回答を参考にし始めるので、精度が低下する場合があります。この場合は、ソースの選択を解除し、リロードすることで回答精度を元に戻すことが可能です。


2. ソースのフォーマット整備が重要

AIが参照元を特定する際、ソースのフォーマットが整っていないと回答の参照元のソースの範囲が広くなります。特にPDFやYoutubeなどをソースに使用した場合、文字起こししただけなのでフォーマットが整っていないです。

対策: ソースを以下のようにマークダウン形式で整備するのが最適です。

例:

---
#### 区分
セキュリティ

**種別**: 準拠性

- **サービスレベル項目**: システム監査頻度
- **規定内容**: システム監査を実施する頻度の規定
- **測定単位**: 年
- **設定例**: 年1回
- **備考**: 必要に応じて追加実施
- **回答**: 年1回、定期的にシステム監査を実施します。

---
#### 区分
セキュリティ

**種別**: 保全性

- **サービスレベル項目**: データ暗号化
- **規定内容**: ユーザーデータの暗号化有無
- **測定単位**: 有無
- **設定例**: 全データ暗号化
- **備考**: 保存時および送信時の両方を対象
- **回答**: ユーザーデータは保存時・送信時ともに暗号化しています。

3. ソースの更新は手動

NotebookLMではソースの自動更新は行われません。そのため、新しい情報を反映するにはソースを再アップロードする必要があります。


4. 質問の工夫が必要

質問に対して一部回答できるものがある場合は、ソースから推測して回答を作成するので、回答が長くなりがちです。ですので、回答を簡潔にするには、質問時に「簡潔に回答してください」を付け加えるのが有効です。
また、質問文ではなくキーワードを提示するだけでも適切な回答が得られる場合があります。

キーワードだけの例:


改善できそうなこと

  • ソースファイルのフォーマットの最適化 現在はマークダウン形式でソースを作成しており、元となったソースの範囲がある程度狭くなりましたが、フォーマット次第ではもっと回答の時のソースの範囲を狭めることができるのではないかと思っています。

Go学習ロードマップを完走!141時間かけて学んだこと

この記事は個人ブログと同じ内容です

www.ritolab.com


Go の理解を深めるために roadmap.sh を利用し学習したので紹介します。

roadmap.sh とは

roadmap.sh は、開発者向けの学習ロードマップを提供するオープンソースプロジェクトです。

フロントエンド、バックエンド、DevOps、データエンジニアリング、AI/ML など、さまざまな分野で必要なスキルを視覚的なロードマップとして整理し、学習の指針を示しています。

ユーザーはロードマップを参考にしながら、自分に必要なスキルを順序立てて学ぶことができるのが特徴です。

OSS のため料金は無料です。そして誰でもロードマップに貢献できます。

kamranahmedse/developer-roadmap - Github

Step by step guide to becoming a Go developer

roadmap.sh の中の、Go の学習ロードマップである Learn to become a Go developer に沿って学習を進めました。

ロードマップと学習内容

Go の学習ロードマップは以下で構成されており、これらについて学ぶことができます。

  1. 基礎を学ぶ (Learn the Basics)
    • 基本構文 (Basic Syntax)
    • 変数と宣言 (Variables and Declaration)
    • データ型 (Data Types)
    • 制御構造 (Control Flow)
    • エラー処理 (Error Handling)
      • Errors, Panic, Recover
    • モジュール管理 (Modules)
      • Go Modules
    • 条件分岐 (Conditionals)
    • 関数 (Functions)
      • 複数の戻り値 / 名前付き戻り値 (Multiple/Named Returns)
    • パッケージ管理 (Packages, Imports, and Exports)
    • 型システム (Type System)
      • 型キャスト (Type Casting)
      • 型推論 (Type Inference)
      • 配列 (Arrays)
      • スライス (Slices)
      • マップ (Maps)
      • 構造体 (Structs)
  2. 応用を学ぶ (Going Deeper)
  3. CLI ツール開発 (Building CLIs)
  4. ORM (Object-Relational Mapping)
  5. Web フレームワーク (Web Frameworks)
  6. ロギング (Logging)
  7. リアルタイム通信 (Realtime Communication)
  8. API クライアント (API Clients)
  9. テスト (Testing Your Apps)
  10. マイクロサービス向けツール (Tools for Microservices)

全学習にかけた時間

Go 学習ロードマップは、全 69 項目ありますが、その中の 55 項目を、 47 の学習機会に分けて進めていきました。

(大分類中の 9 項目は概要説明で手を動かしていないのでカウントから除外、他、スキップした 5 項目(これについては後述)を除く)

「47 の学習機会」は、「47 日取り組んだ」と同義です。

ちなみに 1 回の学習機会は約 2 〜 3 時間。

 \displaystyle
3h × 47 = 141h

およそ 141 時間かけたことになります。

詰め込むなら、1 日 8 時間学習する合宿形式にしたとして

 \displaystyle
141h / 8h = 17.625

18 日間の合宿になります。

私はこの 47 の学習機会を、5 ヶ月間かけて実施しました。平均すると 1 ヶ月 10 機会(30時間)程度です。

Go 学習ロードマップを活用して感じたメリット

1. 必要な知識が整理されている

新しいプログラミング言語を学習しようとしたとき、「この範囲についてはこれとこれだよ」がまとまっているのは学習しやすい点です。

例えば Go の型を学ぼうとしても、いくつ、どんな型があるのかまず自分で収集して、それに漏れがないか調べてやっと学習開始。

では次に制御構造を学ぶから、それをいくつどんなものがあるのかを調べて、、のようなことを毎回やっていると疲れます。

ここがすでにまとまっているだけでも、学習のしやすさは格段に違いました。

2. 全体像が見えるから計画が立てやすい

学習していく順序が示されているのも学習しやすい点です。

新しいプログラミング言語を学習しようとしたとき、どこから、何から学習していこうかに迷わなくなるのは意外と大きなモチベーションに繋がったりするものです。

また、全体の学習計画も立てやすくなります。

必要な知識が整理され、ロードマップとして並べられているからこそ、

「これくらいのペースでやっていく。そうするとこのくらいの期間で学習を終えられる」

「この日までに終わらせよう。それならこれくらいのペースでやっていこう」

のような見通しもつけられました。

3. 学習完了をマークできる UI

地味に推したいのが、学習を終えたトピックを完了にできる点。

サイトにログインすれば、以下のようにそのトピックやセクションを完了にできます。

意外とこういうのって励みになるんですよね。だって一人でロードマップ上を長距離走しているようなものですから。

一つ終了する度に、自分を「よくやった」と褒めて完了をマークしましょう。新しいことを一人で学び続けていくのは、エネルギーとモチベーションが要ることです。

ちなみにこうやってマークした自分の画面はシェアできます。以下は私の Go 学習ロードマップです。

https://roadmap.sh/golang?s=6705c620fb4be684db966395

Go 学習ロードマップを進める前に知っておきたいこと

ロードマップを進めていく上でいくつか気をつけるべき点もあります。

1. 解説は最小限。学習の指針として活用

ロードマップ一つ一つのセクション、トピックには、詳細な解説やサンプルコードがあるわけではありません。

各トピックの詳細画面では、概要の紹介と併せて、公式ドキュメントを中心としたリンクが貼られているだけです。

つまり、そこからは自分で情報を収集し、咀嚼し、理解する必要があります。

学習のコツとしては、そのトピックのリンク先の内容を読み解きつつ、他にもそのトピックについての解説や記事を調べて読んでいくと理解が深めやすいです。

その上で、自分で手を動かして動くコードを書いてみる。

リンクのみは簡素と言われれば簡素ですが、誰の解説であってもハマらないものはハマりませんし、この形態で私は十分でした。

なんだかんだ、公式ドキュメントのリンクは探す手間が省けてありがたかったです。そのトピックについて、学習のスタート地点を示してもらえている感覚でした。

2. 必ずトレンドに沿っているわけではない

ここでいうトレンドは、例えばパッケージや FW で「多くの人に使われている(Githubのスター数、ダウンロード数など)」「開発・メンテナンスが活発である(最終コミット日時)」を満たしていたらトレンドだよね。という観点です。

Go のロードマップは、前半は言語自体の仕組み(型や制御構造など)を学習しますが、後半からパッケージや FW が多くなります。

このとき、各ラインナップは必ずしも現在トレンドであるものではないことには注意が必要です。メンテナンス(最終コミット)が 1 年無いものなどもあります。

つまり、トピックで扱う FW やパッケージについて本当に学習をするかどうかは自分で判断したほうがよいです。

オープンソースのプロジェクトである以上、善意で成り立っている学習プログラムですので、ここについては更新されるタイミングによりますが、学習前に一度そのパッケージについて調べてみてください。

(前述の「全学習にかけた時間」で 5 つのトピックを skip しているのはこの理由からです)

例えば FW であれば、そのトピックで扱っている FW がトレンドであるかは確認しつつ、現在トレンドの FW は何であるか調べると学びの幅も広がります。

Go 学習ロードマップの振り返り

roadmap.sh のトピックは基礎が網羅されているため、しっかり足元を固められます。型や制御構造など、なんとなく使えるよりも、一度体系的に学んでおくことで、自信を持って実装できるようになります。

また、知識を整理しておくと、実装時にも「この記述のほうが適切かもしれない」「このアプローチも考慮すべきか」「本当にこれでいいのか」といった視点を持ちやすくなります。例えば、型アサーションを理解しておけば、interface{} を扱う際に適切なキャスト方法を選べますし、コンテキスト管理を知っていれば、適切なタイミングで処理をキャンセルしたり、タイムアウトを設定したりできるようになります。

加えて、Go の並行処理や Web フレームワーク、ORM などの応用的な知識に触れることで、実際の開発現場でも「より適した手法」を選べる引き出しが増えます。特に、GORMgqlgen, Protocol buffers など、実務でよく使われるツールに触れられるのは大きなメリットです。

全体として、Go の基本から応用までしっかりと学べる内容であり、Go での開発スキルを一段階引き上げるのに最適なカリキュラムだと感じました。

また、前述の通り内容が公式ドキュメントや著名な記事のリンクが中心であることで、自分で学びと理解を探しに行く必要こそあるものの、"陳腐化しない" のは良点です。公式ドキュメントの解説は常に最新バージョンとリンクしています。

昨今、動画による学習も学びの手段の一つとなっています。Go は ver 1.18 を堺に Go Modules が完全に標準化され、GOPATH に依存しなくてもプロジェクトを管理できるようになりましたが、ある有料の動画学習プログラムでは ver 1.18 以前のものであったため、環境構築の方法が古い GOPATH ベースの手順で解説されており、現在の推奨方法と異なっていて困惑した経験があります。Go に限らず、例えば Python の学習動画もパッケージなどのバージョンによる "動かない問題" が結構あったりする印象もありますし、そういった面で見れば少なくとも「この情報は鵜呑みにしてよいのか」の判断が 1 つ少ないのは「知らない者」が「知る者」になるための嬉しいポイントでもあります。

Go 学習の次のステップ

私自身、業務で Go を使用していますが、言語構造の理解を深めそれらを "インプット" することで増える "選択肢" は、実装判断にスピードをもたらしています。レースコンディションの制御や並行処理など、実際に必要になった場面で素早く導入し、より堅いアプローチで実装することができました。

また、包括的に Go に触れられたことでその全体像の輪郭が以前よりは格段に見えるようになりました。この「輪郭」に確固たるラインや数値があるわけではないのですが、ここの輪郭が見えてくるようになると今度は「デザインパターン」であったり「アーキテクチャの設計思想(DDD, Clean Architecture)」に触れていくモチベーションが生まれてきます。現在私の興味関心はこれらです。

Go のエコシステムは日々進化しており、新しいライブラリやツールも次々と登場していますが、本カリキュラムを通じて得た知識は、今後の開発において確実に活かせています。Go を学び始めたばかりの方はもちろん、ある程度の経験がある開発者にとっても、基礎を見直しながら応用力を高められる内容になっています。なぜならば、トピックに対して何を学びとするかは自分自身で決めるからです。

特に Go のようにシンプルながら奥深い言語では、基礎をしっかり押さえた上で、実践の中で適切な選択をする力が求められます。文法はシンプルでも、並行処理、エラーハンドリング、アーキテクチャ設計などを適切に扱うには、深い理解が求められます。このロードマップは、そのための道標として非常に有効でした。学びを積み重ねながら、これからも Go の設計思想や最新のエコシステムに触れ、より良いコードを書けるよう成長していきたいと思います。