RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

AWS Lambda から GCP CloudFunctions/Cloud Run を呼び出す (クライアントライブラリ編)

はじめに

こんにちは、DevOps グループの中島です。
前回 は REST API を自力で叩いて認証を行い、取得した ID Token で Cloud Functions のエンドポイントを呼び出しました。 今回はクライアントライブラリを使用する方法を見ていきたいと思います。 といってもコード自体は数行しかないので、そのコードにたどり着くまでの過程も書いてみます。

google.oauth2.id_token.fetch_id_token を使う

Cloud Functions を呼び出すためには ID Token が必要となるので、まずはこの ID Token を一発で取得できる方法を探していきます。 gcp id token python などで検索すると google-auth というライブラリの ユーザガイドがヒットし以下のような記述があります。

If your application runs on App Engine, Cloud Run, Compute Engine, or has application default credentials set via GOOGLE_APPLICATION_CREDENTIALS environment variable, you can also use google.oauth2.id_token.fetch_id_token to obtain an ID token from your current running environment. The following is an example

import google.oauth2.id_token
import google.auth.transport.requests

request = google.auth.transport.requests.Request()
target_audience = "https://pubsub.googleapis.com"

id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)

書いてある内容を見ると上記のコードでいけそうな気がするので GOOGLE_APPLICATION_CREDENTIALS に、サービスアカウントの 認証情報構成ファイル のファイルパスを指定して試してみます。 補足ですが、Lambda にデプロイしたファイルは /var/task/ に配備されます。

// 実行結果
{
  "errorMessage": "Neither metadata server or valid service account credentials are found.",
  "errorType": "DefaultCredentialsError",
  "requestId": "e01c15bd-50e6-4c5c-913b-02e09d2aada0",
  "stackTrace": [
    ...
    "  File \"/var/task/google/oauth2/id_token.py\", line 295, in fetch_id_token_credentials\n    raise exceptions.DefaultCredentialsError(\n"
  ]
}

Function Logs
START RequestId: e01c15bd-50e6-4c5c-913b-02e09d2aada0 Version: $LATEST
[WARNING]   2024-04-11T06:19:36.615Z    e01c15bd-50e6-4c5c-913b-02e09d2aada0    Compute Engine Metadata server unavailable on attempt 1 of 3. Reason: HTTPConnectionPool(host='169.254.169.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f5294d9bd50>: Failed to establish a new connection: [Errno 111] Connection refused'))
...

しかし、これはうまくいきません。 なぜか Lambda の実行環境には存在しない Metadata server (169.254.169.254) につなぎに行こうとしています。

Lambda で動作していることを認識できていないのかと思い、以下のように aws.Credentials.from_file を使って明示的に AWS の認証情報を与えてもうまくいきません。

from google.auth import aws
from google.auth.transport.requests import AuthorizedSession
import google.auth.transport.requests
import google.oauth2.credentials
import google.oauth2.id_token

def get_id_token(service_account, audience):
    credentials = aws.Credentials.from_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )
    session = AuthorizedSession(credentials)
    request = google.auth.transport.requests.Request(session)
    id_token = google.oauth2.id_token.fetch_id_token(request, audience)

    return  id_token

それもそのはずで、実装 を見に行くと以下のように書かれており、

  1. If the application is running in Compute Engine, App Engine or Cloud Run, then the ID token are obtained from the metadata server.
  2. If the environment variable GOOGLE_APPLICATION_CREDENTIALS is set to the path of a valid service account JSON file, then ID token is acquired using this service account credentials.
  3. If metadata server doesn't exist and no valid service account credentials are found, :class:~google.auth.exceptions.DefaultCredentialsError will be raised.
  1. は GCP 上で動作する場合のみ対象で、頼みの 2. は以下のように読み込んだサービスアカウントの構成ファイルが type = service_account の場合しか対応していません。
with open(credentials_filename, "r") as f:
    info = json.load(f)
    credentials_content = (
        (info.get("type") == "service_account") and info or None
    )

    from google.oauth2 import service_account

    credentials = service_account.IDTokenCredentials.from_service_account_info(
        credentials_content, target_audience=audience
    )

今回ダウンロードした認証情報構成ファイルは type = external_account です。

{
    "type": "external_account",
    "audience": "//iam.googleapis.com/projects/.../locations/global/workloadIdentityPools/.../providers/...",
    "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
    "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/...@....iam.gserviceaccount.com:generateAccessToken",
    "token_url": "https://sts.googleapis.com/v1/token",
    "credential_source": {
        "environment_id": "aws1",
        "region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
        "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
        "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
    }
}

このため、google.oauth2.id_token.fetch_id_tokenを使うのは無理筋です。

IAM credentials の ID Token を取得する API を呼ぶ

google-auth 単体ではダメなのかもしれないと思い、公式の API クライアントを使用してみます。 以下のライブラリを使えるようにした上でコードを実行してみます。

  • google-api-python-client
  • google-cloud-iam
from google.auth import aws
from google.cloud import iam_credentials_v1

def get_id_token(service_account, audience):
    credentials = aws.Credentials.from_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )
    client = iam_credentials_v1.IAMCredentialsClient(credentials=credentials)
    request = iam_credentials_v1.GenerateIdTokenRequest(
        name="projects/-/serviceAccounts/{}".format(service_account),
        audience=audience,
    )
    response = client.generate_id_token(request=request)
    return response.token

これはうまく動作します。ただし、こちら にある通り、このライブラリはメンテナンスモードとなっており、代わりに google-cloud-python を使うように書かれています。 しかし、リファレンス を見ると ID Token を取得する API は実装されていません! メンテナンスモードになったライブラリを使うのは気が引けるのと、余計な依存ライブラリが増えてサイズも大きくなってしまうので、また別の方法を探すことにします。

google-auth 再訪

どうにかして google-auth 単体でできないかと思い、ID Token 取得エンドポイントの generateIdToken で google-auth のコードを grep していると、 このような実装 があるのを見つけました。

_IAM_ENDPOINT = (
    "https://iamcredentials.googleapis.com/v1/projects/-"
    + "/serviceAccounts/{}:generateAccessToken"
)

エンドポイントの URL が書いてあるということは、それを呼んで ID Token を取得しているはずです。 この実装のあるファイル名は impersonated_credentials.py で、どこかで聞き覚えがある単語だと思ったら Workload Identity 連携を調べていたときに 偶然英語で開かれたドキュメントでみたものでした。 GCP ドキュメント上での日本語訳は"借用"です。サービスアカウントを借用して ID Token を生成する の"借用"です!

たぶんこれだということで、うまくいくことを祈りながら ドキュメント を参考に実装してみます。

import google.auth.transport.requests
from google.auth import aws
from google.auth import impersonated_credentials

def get_id_token(service_account, audience):

    # 実行環境 (Lambda) の認証情報を取得
    credentials = aws.Credentials.from_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )

    # サービスアカウントを借用
    target_credentials = impersonated_credentials.Credentials(
      source_credentials=credentials,
      target_principal=service_account,
      target_scopes = ["https://www.googleapis.com/auth/cloud-platform"],
      lifetime=500)

    # ID トークンを取得
    imp_cred = impersonated_credentials.IDTokenCredentials(target_credentials, target_audience=audience)
    imp_cred.refresh(google.auth.transport.requests.Request())

    return imp_cred.token

これで無事 ID Token が取得できました。

ところで弊社には ChatGPT, Gemini, Bedrock を使用できる環境があるので、以下のようなお願いを形を変えて何度かしてみましたが、架空の API を作り上げてしまったりしてうまくいきませんでした。いつか上手に使えるようになりたいものです。

AWS Lambda で動作する Python アプリケーションにおいて、認証が必要な Cloud Functions を呼び出すコードを書いてください。Cloud Functions を呼び出す際のサービスアカウント認証情報は Workload Identity 連携を使用して取得するものとします。

おわりに

今回紹介したコードは、認証が必要な Cloud Functions を Lambda から呼び出す構成において AWS と GCP の設定が問題ないかを検証するために書いたものです。 実際のアプリケーションは Go で実装されこのコードは使われなかったので、ここで供養することにしました。
この記事がどなたかのお役に立てば幸いです。

生成AI のガイドラインを Guardrails for Amazon Bedrock でサポートする

ChatGPT を皮切りにAIチャットボットを導入する企業が増えてきました。Amazon Bedrockは、大規模言語モデル(LLM)を使用したアプリケーション開発のためのプラットフォームであり、最近Guardrailsという機能が追加されました。申し遅れました、CTO室の塚田です。

生成AIを活用する際、適切なガイドラインの設定は欠かせません。Amazon Bedrockの新機能であるGuardrailsは、このガイドラインの設定と実施をサポートするツールです。

Guardrailsでは以下の3つの機能が利用可能です:

  1. トピックの拒否
  2. コンテンツフィルター
  3. 個人情報の再編集

これらの機能は、以下のような問題を回避するために設計されています:

  • ユーザー入力に有害な言葉・内容が含まれている
  • 生成AIの応答に意図せず有害な言葉・内容が含まれてしまう
  • 悪意のあるユーザーによる生成AIへの攻撃により、不適切な応答や情報漏洩が起こる
  • 機密情報や個人情報がユーザー入力や生成AI応答に含まれてしまう
  • 業務上都合の悪い言葉・内容がユーザーと生成AIの会話に含まれる

名前の通り、あらかじめガードレールを設置することで、これらのリスクを軽減できます。

具体的な使用方法は、Amazonの公式ブログ をご参照ください。 公式ブログでは、GuardrailsをBedrockに作成し、"CompetitorY"という単語をNGワードに指定する例が紹介されています。

Pythonを使ってBedrockを実行する際のGuardrailsの使用方法は、ブログには詳しく説明されていませんので Guardrailsを活用したサンプルコードは以下の通りです。

import boto3
import json
import os

client = boto3.client('bedrock', region_name='us-east-1')
bedrock_runtime = boto3.client('bedrock-runtime', region_name='us-east-1')

guardrail_identifier = os.environ['GUARDRAIL_IDENTIFIER']
guardrail = client.list_guardrails(guardrailIdentifier=guardrail_identifier)['guardrails'][-1]

def invoke_model(client, prompt, model,
    accept = 'application/json', content_type = 'application/json',
    max_tokens  = 512, temperature = 1.0, top_p = 1.0, top_k = 200, stop_sequences = [],
    guardrailIdentifier=guardrail['id'],
    guardrailVersion=guardrail['version'],
    trace="ENABLED"):

    input = {
        'max_tokens': max_tokens,
        'stop_sequences': stop_sequences,
        'temperature': temperature,
        'top_p': top_p,
        'top_k': top_k,
        "anthropic_version": "bedrock-2023-05-31",
        "messages": [{"role": "user", "content": prompt}]
    }
    body=json.dumps(input)
    response = client.invoke_model(body=body, modelId=model, accept=accept, contentType=content_type,
                             guardrailIdentifier=guardrailIdentifier,guardrailVersion=guardrailVersion,trace=trace)
    response_body = json.loads(response.get('body').read())
    output = response_body.get('content')[0]['text']
    return output, response_body

def get_output(prompt, model, max_tokens, temperature, top_p):
    output, response_body = invoke_model(
        client=bedrock_runtime,
        prompt=prompt,
        model=model,
        temperature=temperature,
        top_p=top_p,
        max_tokens=max_tokens,
    )
    print("Guardrail Trace:")
    print(json.dumps(response_body, indent=2))

    print("Response:")
    print(output)

prompt = os.environ['MSG']
model = "anthropic.claude-3-haiku-20240307-v1:0"

max_tokens = 1024
temperature = 0.1
top_p = 0.9

get_output(prompt, model, max_tokens=max_tokens, temperature=temperature, top_p=top_p)

この例では anthropic.claude-3-haiku を使うようにしていますが、ここは柔軟に変更が可能です。

GUARDRAIL_IDENTIFIER には Guardrails の ID をブラウザで確認した上で設定してください

guardrails

pip install boto3 --target .
export GUARDRAIL_IDENTIFIER='your-guardrail-identifier'
export MSG='CompetitorY という会社について教えてください'
python guardrails.py

ガードレールが有効な場合、フィルタリングに設定した"CompetitorY"がブロックされ、以下のような結果が返ってきます:

Guardrail Trace:
{
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Sorry, the model cannot answer this question."
    }
  ],
  "amazon-bedrock-trace": {
    "guardrail": {
      "input": {
        "xxxxxxxx": {
          "wordPolicy": {
            "customWords": [
              {
                "match": "CompetitorY",
                "action": "BLOCKED"
              }
            ]
          }
        }
      }
    }
  },
  "amazon-bedrock-guardrailAction": "INTERVENED"
}
Response:
Sorry, the model cannot answer this question.

今回の例はわかりやすいように、NGワードを用いたブロックの例ですが、マスキングすることも可能です。

Guardrailsは、事前に設定したルールに基づいてユーザー入力や生成AIの応答をフィルタリングする機能です。 NGワードや禁止トピックを設定することで、不適切なコンテンツを排除、必要であれば名前、住所、電話番号、Eメールアドレスなどの情報を自動的にマスキングできます。

LLMを使用する際は、有害な言葉や内容、機密情報、個人情報などが含まれるリスクがあるため、Guardrailsのような機能を活用し、適切なガイドラインを設定することが重要です。

Guardrailsを使うことで、生成AIの活用における様々なリスクを軽減し、安全で適切な運用が可能となります。企業におけるAIチャットボットの導入が進む中、Guardrailsは生成AIガイドラインの設定と実施に大きく貢献してくれると考えております。

AWS Lambda から GCP CloudFunctions/Cloud Run を呼び出す (REST API 編)

はじめに

こんにちは、DevOps グループの中島です。
最近タイトルのような Lambda から、認証が必要な CloudFunction エンドポイントを呼び出すアーキテクチャを実装する機会がありました。 認証を行うには、クライアントライブラリを利用する方法 (推奨) と REST API を叩いて自力でやる方法がありますが、 内部の動作を理解してこそライブラリを使う権利が与えられると思いますので、今回は REST API を叩く方法を紹介します。

概要

以下のような流れで認証を行った上で、最終的に Cloud Functions を呼び出します。

引用元: https://cloud.google.com/iam/docs/tutorial-cloud-run-workload-id-federation

  1. Lambda の認証情報を用いて、IdP Token を生成する
  2. IdP Token を用いて、GCP STS から Federated Token を取得する
  3. Federated Token を用いて GCP IAM Credentials から ID Token を取得する
  4. ID Token を用いて、Cloud Functions を呼び出す
用語 説明
IdP Token Workload Identity 連携を使用すると、事前に GCP に Lambda を実行するロールの ARN 等が登録されているため、Lambda の認証情報を用いて GCP STS トークン (Federated Token) と交換できます。 この交換に必要となるトークンです。(参考)
(Workload Identity 連携の詳しい説明や必要なリソース、設定等については他所に譲ります)
Federated Token サービスアカウントの権限を借用 (Impersonation)するためのトークンです。この権限を用いて OAuth 2.0 アクセストークン (ID Token) を取得できます。(参考)
ID Token 認証を要求する Cloud Functions の呼び出しに必要なトークンです。(参考)

1. Lambda の認証情報を用いて、 IdP Token を生成する

IdP Token のフォーマットは こちら の subjectToken に記載があります。

If subjectToken is for AWS, it must be a serialized GetCallerIdentity token. This token contains the same information as a request to the AWS GetCallerIdentity() method, as well as the AWS signature for the request information. Use Signature Version 4. Format the request as URL-encoded JSON, and ...

つまり、GetCallerIdentity の呼び出し情報に SigV4 署名を付与したものが、IdP Token となります。

IdP Token は 仕様 に記載の通り以下の情報を含める必要があります。

項目名 設定値
url AWS STS のエンドポイント: https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15
method POST
headers リクエストヘッダ
authorization SigV4 署名
host sts.amazonaws.com
x-amz-date ISO 8601 形式の現在日時
x-goog-cloud-target-resource //iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<pool-id>/providers/<provider-id>
x-amz-security-token Lambda の実行環境で取得可能な AWS_SESSION_TOKEN

上記を参考に IdP トークンを生成するコードは以下のようになります。

def create_aws_token(project_number, pool_id, provider_id):
    # Request を利用して SigV4 で署名
    request = AWSRequest(
        method="POST",
        url="https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15",
        headers={
            "Host": "sts.amazonaws.com",
            "x-goog-cloud-target-resource": f"//iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/providers/{provider_id}",
        },
    )
    SigV4Auth(boto3.Session().get_credentials(), "sts", "us-east-1").add_auth(request)

    # Request の情報をもとに Token を生成
    token = {"url": request.url, "method": request.method, "headers": []}
    for key, value in request.headers.items():
        token["headers"].append({"key": key, "value": value})

    print(json.dumps(token, indent=2, sort_keys=True))

    return urllib.parse.quote(json.dumps(token))

print の出力は以下のようになり、仕様通りに作れていることが分かると思います。

{
  "headers": [
    {
      "key": "Host",
      "value": "sts.amazonaws.com"
    },
    {
      "key": "x-goog-cloud-target-resource",
      "value": "//iam.googleapis.com/projects/.../locations/global/workloadIdentityPools/.../providers/..."
    },
    {
      "key": "X-Amz-Date",
      "value": "20240410T064224Z"
    },
    {
      "key": "X-Amz-Security-Token",
      "value": "..."
    },
    {
      "key": "Authorization",
      "value": "AWS4-HMAC-SHA256 Credential=.../20240410/us-east-1/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token;x-goog-cloud-target-resource, Signature=..."
    }
  ],
  "method": "POST",
  "url": "https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15"
}

SigV4 署名の詳細は以下の画像のようになるのですが、こちら に詳しい説明がありますので、今回はライブラリの力を借りて詳細は割愛します。

https://docs.aws.amazon.com/images/IAM/latest/UserGuide/images/sigV4-using-auth-header.png

2. IdP Token を用いて、GCP STS から Federated Token を取得する

Federated Token は GCP STS の token エンドポイントから取得できます。API 仕様は こちら です。 audience に Workload Identity の情報を入力し、subject_token に 1. で作成したトークンを使用します。それ以外は規定の値です。

def get_sts_token(aws_token, project_number, pool_id, provider_id):
    url = "https://sts.googleapis.com/v1/token"
    data = {
        "audience": f"//iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/providers/{provider_id}",
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
        "scope": "https://www.googleapis.com/auth/cloud-platform",
        "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
        "subject_token": aws_token
    }
    response = requests.post(url, data=data)

    return response.json()['access_token']

3. Federated Token を用いて GCP IAM Credentials から ID Token を取得する

ID Token は GCP IAM Credentials, projects.serviceAccounts リソースの generateIdToken エンドポイントから取得できます。API 仕様は こちら です。 Authentication ヘッダに 2. で取得した Federated Token を認証情報として設定し、audience に呼び出したい CloudFunctions のエンドポイントを指定します。

def get_id_token(sts_token, service_account):
    url = f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account}:generateIdToken"
    headers = {
        "Authorization": f"Bearer {sts_token}",
        "Content-Type": "application/json; charset=utf-8"
    }
    data = {
        "audience": "https://asia-northeast1-....cloudfunctions.net/...",
        "includeEmail": "true"
    }
    response = requests.post(url, headers=headers, json=data)
    id_token = response.json()['token']
    
    return id_token

4. ID Token を用いて、Cloud Functions を呼び出す

あとは Authorization ヘッダに ID Token を設定して呼び出すだけです。

def call_cloud_functions(id_token):
    url = "https://asia-northeast1-....cloudfunctions.net/..."
    data = {
        "name": "Hello World"
    }
    headers = {
        "Authorization": f"Bearer {id_token}",
        "Content-Type": "application/json"
    }
    response = requests.post(url, headers=headers, json=data)

おわりに

各所に散らばるドキュメントの情報をつなぎ合わせる過程で、認証方式の理解が進んだように思います。 次回はクライアントライブラリ編を執筆する予定です。お楽しみに。

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp

#2 スクラムマスターを雇う時に聞いてみるとよい38個の質問に答えてみる(全5回)

まえがき

レアジョブテクノロジーズの三上です。

2回目の投稿です。前回から1年と4ヶ月振りの投稿です。お待たせし過ぎたかもしれません。

前回の記事 -> #1 スクラムマスターを雇う時に聞いてみるとよい38個の質問に答えてみる(全5回) - RareJob Tech Blog

早速ですが、今回もインタビューを受ける気持ちで直感で答えてゆきます。

スプリントプランニングについて

チームがもっとも価値のあるストーリーに取り組めるようにするためにどのようにスプリントプランニングにスクラムマスターとして貢献するか?

前提としてプロダクトバックログは優先度の高いもの、価値のあるバックログアイテムから順に並んでいるはずです。もしもそうなっていないのなら、まずはその議論からはじめます。

また「価値のあるストーリー」は組織の状況、市場の状況によって変動するものという前提です。各ストーリーの先には組織またはチームが達成したいゴールがあるはずです。毎日忙しいとゴールは見失いがちです。何度でも問いかけて良いと思います。

目指す先があって、チームの目線が揃ってこそ建設的なプランニングができると思うのでそこを意識してファシリテーションします。

ユーザーストーリーの価値をどんなメトリクスに基いて判断するか?どんなメトリクスは受け入れがたいものか?

ユーザーストーリーのWHOに依存すると思いました。例えばエンドユーザーであればCVRやLTVは成果として分かりやすく適切に思いますし、社内の人であればコスト削減の結果の数値だったり可能な限り定量的な目標にしたいです。

リファクタリングや負債解消を目的としたストーリーの場合、前提としてDORAなどのメトリクスを計測できているとプロダクトオーナーと優先度の会話もしやすくなると思います。

そんな我々もDORAメトリクスの計測をはじめました!!(大事)

受け入れがたいものは前述の逆で、抽象度の高いものや誰がどう判断できるのか説明できないものは避けるべきと思いました。

チームのコミットメントの権限を侵犯することなくどのようにもっとも価値のあるユーザーストーリーを選べるようにファシリテーションするか?

コミットメントの権限 の解釈が難しく思いましたが、チームとステークホルダー(PO)の間でデリバリーの確約に対するズレが生じた状況を想像しました。

ここでお互いの達成したいことは何だろうと考えた時に「価値のあるユーザーストーリーを選べる」ように支援することだと思いました。双方の「ズレ」が何なのか観察して問いかけ、解決に導くようにします。このような状況では、あくまで第三者的な観点を持ち、冷静でいることを意識しています。

どれくらいの時間をリファクタリングや重要なバグの修正や新しい技術やアイデアの調査につかうのが適切と考えるか?

正直なところ状況による と思いますが、経験上はリファクタリングが後回しになりがちな状況があるあるだと思いました。この状況の場合は、「ベロシティの3割程度はあえてプランニングに含めずに改善の時間として使う」ということを試したことがあります。この時間がなぜ必要で重要なのかはチーム全体で納得いくように支援します。

チームの個人にストーリーやタスクを割り当てようとするプロダクトオーナーをどう扱うか?

面白い質問ですね。笑

幸いこの状況になったことがありませんが、あるとしたらチームへの信頼がないのだと思いました。チームはあなたが思っているより優秀だし、マイクロマネジメントは必要ないと伝えます。逆にチームが未成熟なのであれば、自律思考になるようにチーム側を支援すると思います。

チームメンバーによるタスクのつまみ食いをどのように扱うか?

絶対にやってはいけない!とは強くは言わないですね。

なぜつまみ食いしたのかを確認して、それが必要なものだったとしたら透明性やベロシティの重要性を伝えます。自分の担当すべきアイテムが完了していて、スプリントゴールも達成しているって前提だったとしたらチームに共有した上でつまみ食いも良いんじゃないかと個人的には思います。

ユーザーストーリーが最終的に確定していないがスプリントの2日目には確定する状況で、プロダクトオーナーはそれをスプリントバッグログに入れようとしている。どのように行動するか?

完成の定義が決まっていないアイテムを計画に入れた場合、意図していないもの=価値の低いものになってしまう可能性があるのでそれを伝えます。また、ストーリーとして確定していないのでポイント見積もりも出来ていないので、他のストーリーが完成できないリスクもあるかもしれない。それでも追加したいですか?とまずは問いかけますね。

どうしても、というならあえて失敗して学ぶのもアリと思う。それがスクラムの強みでもありますし。

スクラムチームのメンバーがスプリントプランニングに参加したがらないだけでなく、時間の無駄だと考えている。このような態度をどう扱うか?

時間の無駄と感じる理由から聞きたいですが、実際にありそうなのは「プランニングが直接自分に関係のない話だ」と感じるとしたら、本当に関係ないか?と問いたい。チームが分業のようになってしまうのはよくあるパターンだと思うので

スクラムチーム内には、サブチームや階層は存在しない。これは、⼀度にひとつの⽬的(プロダクトゴール)に集中している専⾨家が集まった単位である。

というスクラムガイドにある記述を引用しつつ、チーム開発をするメリットを理解してもらえるよう会話します。多くのメンバーがそう感じるとしたらスクラム自体の採用を見直す議論も必要だと思いました。

終わりに

いくつかは回答に詰まるものもあり、改めて考え直す良いきかっけになりました。レアジョブテクノロジーズでの開発に少しでも興味を持っていただけたら嬉しいです。

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp

Buildkit でキャッシュを ECR に保存する

はじめに

こんにちは。DevOps グループの中島です。

弊社では CI ジョブ実行のために EC2 でホストした GitLab Runner (Docker Executor) を利用しており、Docker コンテナ上で CI ジョブを実行しています。 それらのジョブのうちコンテナイメージをビルドするジョブにおいて、時間がかかっている問題がありました。 今回はその問題に対して比較的新しい機能を用いた解決策の紹介と、処理の概要について説明したいと思います。

背景

ビルドに時間がかかっている理由としては、レイヤーキャッシュが利用されていないのが 1 つの要因としてあげられました。 コンテナビルド用の Docker は dood (Docker outside of docker) を採用しており、レイヤーキャッシュは EC2 上に保存されますが、Docker のキャッシュは EC2 の容量の問題から毎日削除しています。 もし削除していないにしても、SaaS のような実行環境ではどのサーバで CI ジョブが実行されるかは定まらず、ローカルのキャッシュは利用できないと考えるのは妥当といえます。

この問題の解決策として、AWS の記事 で紹介されていた ECR にキャッシュを保存する方法*1 を利用しビルド時間の削減を達成することができました。

実装

以下に実装のポイントを説明します。

gitlab-ci.yml

build:
  image: docker:25-rc
  script: ...

ECR へのキャッシュの保存に対応しているのが Docker のバージョン 25 からのため、CI ジョブを実行するコンテナには docker:25-rc を指定します。(現状では正式リリースされていないため rc を利用しています)

script

# キャッシュの格納先 ECR
REPO_URI=<account-id>.dkr.ecr.<my-region>.amazonaws.com/<repository_name>

# 1. Buildkit コンテナの作成
docker buildx create --use --name <buildkit_container_name>

# 2. キャッシュの格納先を指定してビルド
docker buildx build -t ${REPO_URI}:<image_tag> \
  -t ${REPO_URI}:latest \
  --load \
  --cache-to mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=${REPO_URI}:cache \
  --cache-from type=registry,ref=${REPO_URI}:cache .

# 3. Buildkit コンテナの削除
docker buildx rm <buildkit_container_name>

script 内のポイントは以下のとおりです。

  1. docker buildx create --use で Builder の driver として、デフォルトの docker ではなく、docker_container を作成して利用する指定をします。(docker driver では インラインキャッシュ しか対応していません)

  2. docker buildx build でキャッシュの格納先を指定してビルドします。

オプション 説明
--load ローカルに保存する指定 (通常の docker build と異なりデフォルトではどこにも出力しないため、これを指定しないと warn が出ます)
--cache-to mode=max 最終レイヤだけでなく、全てのレイヤをキャッシュの対象とする
image-manifest=true,
oci-mediatypes=true
ECR に格納できる形式の指定
type=registry,ref=${REPO_URI}:cache キャッシュ格納先(書き込み)の指定
--cache-from type=registry,ref=${REPO_URI}:cache キャッシュ格納先(読み込み)の指定

以上の実装で、一度ビルドしたレイヤーが ECR に保存され、次回以降のビルドではそのキャッシュが利用されるようになります。

ビルドの全体像と処理の流れ

GitLab Runner 上では Docker コンテナで CI ジョブが動いていて、そのジョブが Buildkit 用の Docker コンテナを起動していたりします。 一応動作はしているものの、実際にはどのように各コンポーネントが連携して動いているのか全体像が把握できてないことから、どこの設定を変えれば何が変わるのかの確信が持てなかったため、少し詳しく調べてみました。 以下に図を用いてビルドの過程を示しています。

GitLab Runner Server 上では Docker が起動しています。gitlab-runner は CI ジョブ実行のため、Docker Engine API を用いて CI Job Container を起動します。

ホストの /etc/gitlab-runner/config.toml で CI ジョブは /var/run/docker.sock をマウントするように設定しているため (dood)、コンテナ内のジョブはこのソケットを経由してホストの dockerd にアクセスすることができます。

docker buildx create で docker_container の Builder (Buildkit Container)を作成します。コンテナの起動は前述したとおりマウントしたソケットを経由し Docker Engine API を用いてホスト側で行われます。

Buildkit Container が作成されました。このコンテナは /var/lib/buildkit 配下にキャッシュなどを保存しますが、このパスはホスト側のファイルシステムにマウントされています。

docker buildx buildでビルドを実行します。ci-job 内で buildx がクライアントとして buildkitd と通信し、Buildkit Container 内でイメージのビルドが実際に行われます。ビルドの出力は CI Job Container に返却されます。

docker buildx build--loadを指定しているので、イメージを読み込む Docker Engine API を呼び出し dockerd にイメージを保存するよう依頼します。 その後、同様にdocker push で保存したイメージをリモートにプッシュするよう依頼します。

終わりに

CI ジョブでイメージをビルドする処理の流れを追うことで、もやもやしていた感じがスッキリしました。自信を持って Buildkit を使えるようになった気がします。 このポストが誰かのお役に立てば幸いです。

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp

*1:イメージにキャッシュを埋め込むインラインキャッシュを利用するという方法もありますが、マルチステージビルドでは利用に制限があるため採用しませんでした。

ジョブスケジューラの導入

はじめに

こんにちは、DevOps グループの中島です。
年末年始は前からやろうと思っていたエルデンリングをプレイしました。DLC もそのうち出るらしいので楽しみです。
今回はジョブスケジューラの導入をしたときの話をシェアします。

背景

エンタープライズ向けでは、サーバの監視とジョブの管理を行うツールとして、日○や富○通の製品が業界標準として選ばれることが多いように思います。 一方で OSS だと有名どころは Rundeck あたりが候補になるでしょうか。 弊社では AWS を利用しているためその中で見繕うことをまずは考えたのですが、それ専用に提供しているサービスはありません。

そこで、要求仕様を挙げ、最適なものを OSS から選定することにしました。

要求仕様

必要な要件としては以下で、cron を画面操作に置き換えられれば良いという簡単なものとなっています。

  • 社内サーバで動作している cron を置き換えたい
  • サーバにログインせず、GUI から実行や実行結果の確認がしたい
  • ジョブのグルーピングはしたい
  • ジョブ定義のバックアップができる
  • 実際のサービス提供では利用しないため、特段 SLA は高くない

比較

今回は OSS のジョブスケジューラである、Rundeck, Dkron, Cronicle を比較してみました。

Rundeck Dkron Cronicle
開発元 PagerDuty Distributed Works PixlCore
GitHub Stars 5.2k 4k 3k
WebUI あり あり あり
言語 Java Go NodeJS
グルーピング あり なし あり
ジョブ定義のエクスポート あり なし あり

Rundeck は開発元がメジャーなため長期のサポートが期待出来そうです。また、導入事例が多くありトラブルが発生した際の対応が比較的容易と考えられます。 ただ、導入してみた感想としては、とにかく機能が豊富で、ジョブの定期実行をしたいだけなのに設定する必要のある項目が多くあり、学習コストが高いなと感じました。 GUI のデザインも少々分かりにくく(個人の感想です)、動作もサクサクという感じではありませんでした。

Dkron については Go で作られている点や、This is the only job scheduler in the market with truly no SPOF と謳っている部分が魅力に感じました。 しかし、求めているグルーピングやバックアップの機能が、商用サポートを購入しなければ利用できなかったり、そもそも存在していなかったので今回は見送りました。 高 SLA を求められる場合には選択肢に入るかもしれません。

Cronicle は、Dkron と同様に開発元があまり知られていないという懸念はあるものの、今回の要求仕様を満たしています。 それに GUI もシンプルで美しく(個人の感想です)、直感的に操作ができ、必要十分な情報を表示してくれていました。 機能性や拡張性、保守性はそれほど高い要求がされていないことから、今回は利便性の高い Cronicle を採用することにしました。

Cronicle の UI

終わりに

Cronicle を導入してから数ヶ月経ちますが、特に問題なく動いています。 ジョブスケジューラって探してみると意外とこれってのが無いんですよね。 もし導入を検討していたら、Cronicle を選択肢に入れてみて下さい。

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp

EdTech LLM元年でした!という話

どうもCTOの @jumboOrNot です。 今年は怒涛のLLMの年でしたね〜、弊社でもさまざまなトライがあり振り返ってみようと思います。

「見(ケン)」の時期

2022年11月にChatGPTが公開されて、3月にAPIが公開されるまでは私を含め弊社でも関心を持ったり注目する人は増えていましたが、実際にプロダクトへ反映するまでは割と慎重になってしまったかなと思っています。様子見をしつつ、いいプロダクトへの組み込みはなんだろうな〜と考えることが多く、あまり手数を持てていませんでした。まさに「見(ケン)」の時期で手を組んで唸ってました。しかし歩みが遅いと言われていた EdTech 界隈でも想像の何倍ものスピードでさまざまなリリースが増えてきて、だいぶ危機感を持ったことと、技術顧問でもある広木さんからも支援をいただき弊社でもプロダクトへ早期に組み込むこととなりました。

プロダクトへの組み込み

xtech.nikkei.com

思い立ってから1ヶ月、社内外の協力を得て「レッスンAIアシスタントβ」のリリースを実施できました。弊社でしかできない・わかりやすい顧客課題・LLMだからできることを考えての最初のリリースでした。弊社の PROGOS と組み合わせて、レッスン中にレベルに合った学習の支援をする仕組みでした。 多くの方からフィードバックもいただき、APIをstreamで利用できてないことでレスポンスが遅くレッスン中というシチュエーションで使い勝手がまだまだ良くないことや、プロンプトの改善などリリースしてきて見えてきたことが本当に多かったです。 またそれだけでなく「LLMを使ったプロダクト開発」における注意しなければいけないことや、必要なこと、課題なども多く見えてきてその後の開発に活かせる知見を多く得られました。

社内向けのガイドライン・イントラ内にChatGPT

また社内での活用という観点でも、早期にガイドラインを策定し直接ChatGPTを触るのでは無く、統制やユースケースを計測するために社内に ChatGPTをラップしたシンプルなチャットアプリケーションを展開しました。社内でもアンケートを取り、活用におけるハードルや関心、課題を抽出しました。

マナビアップデートソン2023

rarejob-tech-dept.hatenablog.com

初のハッカソンをオフラインで開催し、ここでもLLMを活用した多くのアイデアやプロトタイプが生まれました。 私は学生の頃にさまざまなハッカソンに出てきたんで流石に楽勝かと思いましたが、各チームともめちゃくちゃいいアイデアが多くて結構接戦する結果となりました・・・! 各チームもそうですし、LLMをはじめ技術としても価値が高いものがスピード感を持って組み込める時代になったと感じました。

CTO室の立ち上げ

弊社ではグループでさまざまなプロダクトを運用しており、なかなか横断した支援や改善がしにくいところもありました。 今期はCTO室を立ち上げて、すでにあった弊社のEdTechLabチームと協業し、各チームの支援やR&Dを推進しました。

新プロダクトのローンチ

rarejob-tech.co.jp

K12領域(幼稚園1年と12年間の初等・中等教育を含めた13年間の教育期間)のユーザーに対して、覚えた単語でオリジナル絵本を作成し、親子で楽しむカスタマイズ絵本サービス「WordWiz」をリリースしました。ここはCEO、CIO主導で企画からリリースまでメンバーを巻き込みながら進めてくれていました。 CTO室としてもこのPJに参画してもらい実際に手を動かしてもらったり、ユーザーヒアリングなどを実施していきました。

社内での活用の促進

  • OpenAI APIのお試し用・検証用のapikeyの発行、取得手順公開
  • 社内向けのLLM利用ガイドラインの準備・公開
  • 社員向けのprivate LLMの準備(Amazon Bedrock)
  • 相談用のchannel作成、アイデア募集
  • working groupの発足
  • Github copilotの導入

etc...

とにかく今年は手数だなと思っていたので触れる機会を多く社内でも作りました。

Amazon Bedrock の活用

機会をいただき弊社でも Amazon Bedrock を活用したさまざま検証を進めた結果、re:invent にロゴを掲載いただくことができました。 (まさかの私のミスでレアジョブテクノロジーズで無くて、親会社のレアジョブロゴを渡してしまい、そっちが載ってます・・・なんて親孝行なんでしょうか(違う)) 勉強会もお誘いいただき弊社メンバーにも参加してもらい(様子もブログに書いてもらいました)、弊社でもこれを使ったサービス企画やプロトタイピングがはじまりました。

そして来年

この1年、LLMのもたらす怒涛のリリースにいい意味で焦燥感を得ながら考えていたことは「人とAIが共創して価値を作ること」だなと思いました。 もちろん人の仕事をAIが奪っていくようなことは増えていく一方で、あらためてAIだけでなく「人が何をなすべきか」が EdTech サービスを提供する弊社でも再定義が必要なことだと思いました。

真面目な記事になってしまったので、ここらで共創のトライとして締めのジョークはLLM君に任せてみようと思います。

・・・

スベッてますね、バナナだけに。

まだまだ価値の創出までは遠いですが、回り道をしながらでもトライの数を増やし続けたいと思います。 良いお年を、ウホウホ