Y_Yamashitaのブログ

勉強したことのアウトプット・メモが中心。記事の内容は個人の見解であり、所属組織を代表するものではありません。

2026年3月の振り返り

早いもので、今年もあっという間に4分の1が終わってしまいました。。。危機感を覚えつつ、2026年3月の振り返りをしたいと思います。

インプット

AWSさん主催の以下のウェビナーに参加しました。

aws-experience.com

また、下記リンクのものではないのですが、別日にあった同様のワークショップに参加しました。

aws.amazon.com

ただ、どちらも単発のイベントであり、継続的なインプット学習が出来ていないので、ここは反省ですね。

アウトプット

技術ブログ

3月は3本ブログを書きました。月2本くらい書ければと思っているので、ここは良かったです。

yuy-83.hatenablog.com

yuy-83.hatenablog.com

yuy-83.hatenablog.com

下の2つは検証した日にそのまま勢いで書きました。このやり方は自分に合ってそうだったので、今後も継続出来ればと思います。

LTや登壇

先月に引き続き、LTや登壇は行いませんでした。そろそろ何かやらないと。。

運動

ジム

3月のジムの入館回数は8回でした。
1月が14回、2月は10回だったので、じりじりと減っていってますね。。これはマズい。。。立て直さなくては。

有酸素運動(ウォーキング)

アプリの記録によると、80.5㎞でした。
こちらはジムとは対照的に、1月から、47.5 → 68.5 → 80.5 と微増してきています。また、今月は筑波山に登ったので、その分も含まれています。
暖かくなってきたし、4月はさらに運動量を増やしたいと思います。

ストレッチ

ストレッチは相変わらずやったりやらなかったりですね。。ケガの予防や疲労回復のためにも、非常に重要なのですが。。

生活習慣

相変わらず、ダラダラYouTubeやSNSとかを見て夜更かしする日が結構ありました。。もしかしたら「報復性夜更かし」「リベンジ夜更かし」と呼ばれるものなのかもしれません。ただ、私の場合はその時間を楽しいと感じているわけではなく、惰性で続けていることがほとんどなので、何とかしたいですね。ブロックアプリとかを使って、深夜にはSNSを起動できなくするとか、何かしらの手を打ちたいと思います。

まとめ

冒頭にも書いた通り、今年も4分の1が終わってしまいました。この3か月、一応は新しい知識を得たり、新しい経験をしてはいるのですが、いかんせん世の中の進化が速すぎて、自分は遅れていっている感覚が強いです。また、今の運動量や生活習慣だと、体力の下降も加速してしまいそうです。
このままだと、年末に「何か今年はイマイチな1年だったな。。」と思ってしまう未来が見えるので、気を入れ直して「良い1年だったな」と思えるようにしたいですね。

(dify-self-hosted-on-aws)閉域網のAWS環境で、Dify公開アプリにユーザー認証を導入する(Nginx+AD)

皆様は、セルフホスト版コミュニティプランのDifyを閉域網のAWS環境で利用していて、公開アプリにユーザー認証を導入したいと思ったことはありますか?
あ、ないですか。。。まあそうですよね。閉域網であれば、全くの第三者にアクセスされるリスクは低いですもんね。。。
けど、私はあるんです。閉域網といえども全く安全というわけではないし、公開アプリの内容(扱うデータ)によっては、社内の利用者も限定したいケースがあると思います。
そこで今回は、NginxとActive Directory(以下、AD)を利用したユーザー認証を導入してみます。

なお、「秘匿性の高いデータを扱うようなアプリを業務利用するならエンタープライズプランにしろ」などどいう、的確過ぎるツッコミは無しでお願いいたします。

はじめに

今回のDify環境

今回は、以下のAWS公式サンプル集「dify-self-hosted-on-aws」で環境構築しました。

github.com

こちらのサンプルは閉域網にも対応しているため、今回は既存の閉域VPCにデプロイしました。
なお、こちらはセルフホスト版コミュニティプランが前提ですが、この場合、公開アプリのURLが分かっていてネットワークの疎通性があれば、誰でもアプリにアクセスできてしまいます。(エンタープライズプランには公開アプリへのアクセス制御が備わっています)

(参考)インターネットアクセス環境の場合

インターネットアクセス環境の場合、以下の記事のように、ALB+Cognitoで認証する手があります。
CognitoのホステッドUIを使ってお手軽にユーザー認証を実装できます。

blog.serverworks.co.jp

aws.taf-jp.com

ただし、閉域網の場合、全く同じ方法を取ることは出来ません。Cognitoは2025年11月にVPCエンドポイントに対応したものの、ホステッドUIはサポート対象外となっています。

aws.amazon.com

そこで今回はALBの手前にNginxを導入し、ADと連携して認証してみます。

今回の検証構成

以下の3つのEC2を用意します。

  • 踏み台Windows Server(公開アプリにアクセスするクライアント)
  • 認証用Windows Server(ADを稼働する)
  • Nginx用Linux Server(Nginxを稼働する)

dify-self-hosted-on-awsでは、Dify用ECSの環境変数で、コンソールWEBと公開アプリのURLを変更できます。今回はコンソールWEBを「www.dify-test-domain.local」公開アプリを「app.dify-test-domain.local」とし、Route 53で公開アプリだけNginxに向くようにします。(行きつく先は同じECSですが)

今回の検証構成

各種設定

認証用Windows Server(AD)

今回使用したAMIは「Windows_Server-2022-English-Full-Base-2026.03.11」です。同じAMIでなくとも、Windows Server 2022であれば問題ないかと思います。
ADの設定は、以下のページの設定内容をほぼそのまま真似させていただきました。変更したところといえば、ルートドメイン名を「ad.dify-test-domain.local」にしたくらいでしょうか。

anything-it.info

その後、ADにユーザーを一人追加しました。

PowerShellでユーザー情報を取得してみます。

PS C:\Users\Administrator> Get-ADUser -Identity yamashita -Properties DistinguishedName, sAMAccountName, userPrincipalName                                                                                                                      

DistinguishedName : CN=yuki yamashita,CN=Users,DC=ad,DC=dify-test-domain,DC=local
Enabled           : True
GivenName         : yuki
Name              : yuki yamashita
ObjectClass       : user
ObjectGUID        : 6f13b82d-9a5d-4a8e-af3e-10e1414d46bf
SamAccountName    : yamashita
SID               : S-1-5-21-485893957-3506762766-3137969428-1103
Surname           : yamashita
UserPrincipalName : yamashita@ad.dify-test-domain.local



PS C:\Users\Administrator>

あとは、閉域網かつ検証環境なので、Windows Firewallは全てOFFにしてしまいました(非推奨)。また、セキュリティグループで、Nginx用Linuxからのポート389のアクセスを許可します。389ポートは平文のLDAPです(これも非推奨)。

ADの設定は以上です。

Nginx

Nginxについては、以下のモジュール群の ngx_ldap_auth を使用しました。

github.com

モジュールの導入方法については上記リポジトリ内に記載されていますので省略します。直リンクは以下です。

https://github.com/iij/ngx_auth_mod/blob/master/docs_ja/GettingStarted.md

リポジトリ中の手順と今回自分が実施した手順で異なる箇所だけ記載しますと、「nginxのソースからインストール」は不要でした。 sudo yum install nginx でインストールしたら、auth request moduleも一緒にインストールされたためです。

最終的に、設定ファイルは以下のようになりました。まずはnginx.confです。

/etc/nginx/nginx.conf(タップ・クリックで展開)

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    # ngx_ldap_auth upstream configuration
    upstream ldap_auth {
        server 127.0.0.1:9200;
    }

    server {
        listen       80;
        listen       [::]:80;
        server_name  app.dify-test-domain.local;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        # Exact match for root path - serves index.html
        location = / {
            index index.html;
        }

        # Exact match for /index.html - serves index.html
        location = /index.html {
        }

        # LDAP authentication endpoint (internal use only)
        location = /auth-ldap {
            internal;
            proxy_pass http://ldap_auth;
            proxy_pass_request_body off;
            proxy_set_header Content-Length "";
            proxy_set_header X-Original-URI $request_uri;
        }

        # Protected location - requires LDAP authentication
        location /chat/ {
            # Enable LDAP authentication for /chat/* paths
            auth_request /auth-ldap;
            
            # Pass authentication error to client
            error_page 401 = @error401;
            
            # Proxy to backend
            proxy_pass http://internal-dify-test-tk-alb-xxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Authentication error handler
        location @error401 {
            return 401 "Authentication Required\n";
            add_header Content-Type text/plain;
        }

        # Proxy configuration for all other paths (no authentication)
        location / {
            proxy_pass http://internal-dify-test-tk-alb-xxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Error page configuration
        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
    }
}

パスが / もしくは /index.html で完全一致した場合はNginxのデフォルトのindex.htmlを表示させるようにしています(動作確認用)。パスが /chat/ で前方一致した場合は、認証を行い、OKだったらALBに転送するようにしています。なお、今回はチャットボットを公開して検証したため /chat/ としましたが、公開アプリの種類によってパスは変わりますのでご注意ください。最後に、どれにも一致しなかったものはそのままALBに転送しています。公開アプリを利用する場合、 /favicon.ico といったパスにもリクエストが飛ぶためです。



続いて、auth-ldap.confです。

/etc/ngx_auth/auth-ldap.conf(タップ・クリックで展開)

# ngx_ldap_auth configuration for Active Directory authentication
# Fixed configuration based on actual AD user DN

# Socket configuration
socket_type = "tcp"
socket_path = "127.0.0.1:9200"

# Cache configuration (disabled for security)
cache_seconds = 0
neg_cache_seconds = 0

# ETag-based cache validation (disabled)
use_etag = false

# Serialize authentication per account
use_serialized_auth = false

# Authentication realm (displayed in browser login dialog)
auth_realm = "社内認証"

# Active Directory LDAP connection settings
host_url = "ldap://10.0.0.85:389"

# StartTLS configuration (0 = disabled for plain LDAP)
start_tls = 0

# Certificate verification
skip_cert_verify = 0

# Active Directory domain configuration
base_dn = "DC=ad,DC=dify-test-domain,DC=local"

# Bind DN template
bind_dn = "%s@ad.dify-test-domain.local"

# Unique filter for user validation
# This will match the sAMAccountName and return the full DN
uniq_filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=%s))"

# LDAP connection timeout in milliseconds
timeout = 5000

# HTTP response configuration
[response.ok]
code = 200
message = "Authorized"

[response.unauth]
code = 401
message = "Not authenticated"

host_url に 認証用Windows Serverを指定しています(今回はIP直接指定)。また、 bind_dn = "%s@ad.dify-test-domain.local" としているので、ログインの際は @ad.dify-test-domain.local の入力は不要となります。



Nginxの設定は以上です。

動作確認

それでは動作確認してみます。踏み台Windowsのブラウザからアクセスしてみます。

まずは http://app.dify-test-domain.local/ にアクセスしてみます。

想定通り、Nginxのデフォルトのindex.htmlが表示されました。

続いて、公開アプリのURL http://app.dify-test-domain.local/chat/**** にアクセスしてみます。

ユーザー名とパスワードの入力欄が表示されました。ADに登録したユーザーの名前とパスワードを入力して、「Sign in」ボタンを押してみます。

緊張の瞬間…


無事にアクセスできた

無事にアクセスできました!アプリがちゃんと使えるか試してみます。

どうやらちゃんと応答してくれているようです!


ログ確認

最後に、Nginxのアクセスログを見てみます。

/var/log/nginx/access.log(タップ・クリックで展開)

[ssm-user@ip-10-0-0-75 ~]$ sudo cat /var/log/nginx/access.log | grep yamashita
10.0.0.82 - yamashita [24/Mar/2026:15:03:55 +0000] "GET /chat/xxxxxxxxxxxxxxxx HTTP/1.1" 200 82566 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
[ssm-user@ip-10-0-0-75 ~]$ sudo tail /var/log/nginx/access.log
10.0.0.82 - - [24/Mar/2026:15:03:59 +0000] "GET /_next/static/chunks/2764.23f79cf283ebce5d.js HTTP/1.1" 200 559 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:03:59 +0000] "GET /_next/static/chunks/7194.663b93042d9d84a4.js HTTP/1.1" 200 4146 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:03:59 +0000] "GET /_next/static/chunks/33144.ae1e9106bf795513.js HTTP/1.1" 200 18142 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64;x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/chunks/66687.892a3062a44b823a.js HTTP/1.1" 200 2504 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/chunks/79812939.d3e9cafeee663742.js HTTP/1.1" 200 76995 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/chunks/35303-939a714207b32952.js HTTP/1.1" 200 14303 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64;x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/css/6112f22ea1b7f906.css HTTP/1.1" 200 936 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/chunks/56250.2f52e31f001cfac1.js HTTP/1.1" 200 21605 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64;x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/chunks/42041-c3e1399d4a4176cd.js HTTP/1.1" 200 336971 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
10.0.0.82 - - [24/Mar/2026:15:05:03 +0000] "GET /_next/static/chunks/57538.30a261ff3c131d8f.js HTTP/1.1" 200 592714 "http://app.dify-test-domain.local/chat/xxxxxxxxxxxxxxxx" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0" "-"
[ssm-user@ip-10-0-0-75 ~]$

ユーザー名が記録されるのは認証した時のログだけのようです。ただ、送信元IPで突合すれば、他のログも誰のアクセスなのか判別することは出来そうです。


おわりに

というわけで、閉域網のAWS環境でも、Dify公開アプリにユーザー認証を導入することが出来ました。閉域+検証環境のため暗号化を怠るなど、粗い部分がある点はご容赦いただければ幸いです。(※本記事の内容を参考にされる場合は、出来ればLDAPSを使用するなどカスタマイズいただければ幸いです。本記事の設定をそのまま流用される場合は、リスクをご認識いただいたうえで、自己責任でのご使用を何卒お願いいたします。)

内容のほとんどが Nginx+AD で、Difyは最後の動作確認でしか登場しないので、タイトル詐欺と言われないか心配になりつつ、本ブログを終わります。

【Difyセルフホスト版】Difyから確認コードがメール送信できない状況で、無理矢理ユーザーのメールアドレスを更新してみた

皆様は、Difyから確認コードをメール送信できない状況で、Difyユーザーのメールアドレスを更新したくなったことはありますか?
あ、ないですか。。。まあそうですよね。Difyがメール送信できない状況であれば、そもそも登録しているメールアドレスが使われることはないですもんね。。。
けど、私はあるんです。たとえ使われないとしても、実際のアドレスと違うと管理上なんか気持ち悪いじゃないですか。そんなわけで、今回は無理矢理ユーザーのメールアドレスを更新してみました。


前置き

そもそもDifyとは

ノーコードでAIアプリを作成できるプラットフォームです。チャットボットやエージェントなどが作成できます。詳細は以下の公式ページをご覧ください。

dify.ai

そんなDifyには、SaaSとして利用するパターンと、サーバーにDifyをインストールして利用するセルフホスト版があります。
また、SaaS・セルフホストそれぞれに料金プランが設けられており、無料で利用できるプランもあります(ただしいくつか制限事項あり)
こちらも詳細は公式ページをご参照ください。

dify.ai


dify-self-hosted-on-aws

Difyのセルフホスト版は、もちろんAWSのEC2やECS上で動かすことも可能です。しかも、AWSが、DifyをAWS上ですぐに使えるサンプルCDKを公開しています。(ありがてえ。。)
今回はこちらを使用してDify環境を構築しました。

github.com

上記のREADMEに記載の通り、オプションでDifyにメール送信機能をセットすることも可能です。このメール送信機能は、ユーザーがメールアドレスを更新する際に、6桁の確認コードを送信するのに使用します。つまり、メール送信機能をセットしないと、ユーザーは6桁の確認コードを受け取ることができず、アドレスの更新が出来ないというわけです。


確認コードの確認方法

前置きが非常に長くなりましたが、ではどうするかというと、確認コードはキャッシュに保存されるため、そこから取り出します。dify-self-hosted-on-awsの場合、ElastiCacheに格納されます。

引用元:https://github.com/aws-samples/dify-self-hosted-on-aws


実際にやってみる

それでは、実際にメールアドレスを変更してみます。今回は別途EC2を用意していたので、そこからvalkey-cliで確認コードを取り出してみます。


EC2からElastiCacheにログインする

Difyのアプリ上で確認コードを発行する前に、EC2からElastiCacheに接続しておきます。確認コードの有効期間が5分間しかないためです。
まず、dify-self-hosted-on-aws をそのままデプロイした場合、ElastiCacheのセキュリティグループはECSからのアクセスしか許可していません。そのため、今回はEC2からのアクセスも追加で許可しました。許可するポートはTCP:6379です。

次に、EC2にValkeyをインストールします。

sudo yum install valkey

ElastiCacheの画面でエンドポイントを確認して、EC2から接続します。

プライマリエンドポイントをコピー。ただし末尾の「:6379」は除外する。

[ssm-user@ip-10-0-0-75 ~]$ valkey-cli --tls -h master.dify-test-tk-redis-01.xxxxxxxxxxx.cache.amazonaws.com -p 6379
master.dify-test-tk-redis-01.xxxxxxxxxxx.cache.amazonaws.com:6379>

これで接続自体は出来ましたが、このままでは認証されていないのでコマンドがDenyされてしまいます。dify-self-hosted-on-awsの場合、ElastiCacheの認証パスワードはSecrets Managerに保存されています。シークレットは3つほど作成されるので、ECSの環境変数からリンクを踏むのが一番確実かもしれません。

ECSコンテナの環境変数「REDIS_PASSWORD」にシークレットのリンクがある

リンク先のシークレットにパスワードが保存されている

認証パスワードをコピーしたら、EC2上で認証を行います。「OK」と出れば認証成功です。

master.dify-test-tk-redis-01.xxxxxxxxxxx.cache.amazonaws.com:6379> AUTH oPpjxxxxxxxxxxxxxxxxxxxxxxxZD0
OK
master.dify-test-tk-redis-01.xxxxxxxxxxx.cache.amazonaws.com:6379>


Difyアプリ上で確認コードを発行する

それではDifyアプリ上で確認コードを発行します。アカウント画面からメールの変更ボタンを押します。

すると「メールアドレスを変更」ウィンドウが出てきますが、ここですぐに「確認コードを送信」を押さずに、ブラウザの開発者ツールを開きます。 ChromeやEdgeならF12で開けるかと思います。

すぐに「確認コードを送信」ボタンは押さない

開発者ツールの中に「Network」タブがあるので開きます。

開発者ツールのNetworkタブを開いておく

この状態で「確認コードを送信」ボタンを押すと、Networkタブに「chage-email」と表示されるのでこれをクリックします。

change-emailをクリックする

Responseの中にjsonが格納されており、その中にUUIDがあるのでこれをコピーします。

"data"キーに対するバリューのUUIDをコピーする

EC2からElastiCacheの確認コードを取り出す

再度EC2に戻り、ElastiCacheから確認コードを取り出します。先ほどコピーしたUUIDを使用します。

master.dify-test-tk-redis-01.xxxxxxxxxxx.cache.amazonaws.com:6379> GET change_email:token:59f4ca45-ce80-4932-bdd0-7b1f0020f6ab
"{\"account_id\": \"95cffde3-ec52-4882-9623-c48670aad494\", \"email\": \"test1@test.com\", \"token_type\": \"change_email\", \"code\": \"270909\", \"old_email\": \"test1@test.com\"}"
master.dify-test-tk-redis-01.xxxxxxxxxxx.cache.amazonaws.com:6379>

「 \"code\": \"270909\"」の部分が確認コードです。


Difyアプリ上で確認コードを入力する

再度Difyアプリに戻り、取得した確認コードを入力し、「続行」ボタンを押下します。

成功すると、新しいメールアドレスを設定する画面になります。ここで新しいメールアドレスを入力しますが、ここでもすぐに「確認コードを送信」を押さずに、ブラウザの開発者ツールを開いておきます。 再度確認コードが必要となるためです。変更後のアドレスにも確認コードを送るって、なかなか用心深いですね。。

ここでも「確認コードを送信」はすぐに押さずに開発者ツールを開いておく

同じ要領で、UUIDを取得し、EC2からElastiCacheの確認コードを取り出します。(2回目のEC2のログを取り損ねました。。すいません)

同じ要領で再度UUIDをコピーする

確認コードを取り出したら入力し、「<新しいメールアドレス> に変更」ボタンを押下します。

成功するとログイン画面に戻るので、新メールアドレスとパスワードを入力し、「サインイン」ボタンを押下します。

ログインが成功し、新しいメールアドレスが表示されていれば成功です。

これで無事メールアドレスが変更できました。


(補足)メールアドレスはどこに格納されているのか

そもそもユーザーのメールアドレスはどこに格納されているのかですが、DB(dify-self-hosted-on-aws の場合はAurora)の「accounts」というテーブルに保存されています。(言語やタイムゾーン、最終ログイン日時なども格納されているようです。)

ということは、このテーブルの情報を直接書き換えれば、メールアドレス変更は出来るのでしょうか?
一応出来そうですが、メールアドレス変更時には他の処理(Google/GitHubといった外部認証用のデータ削除)もあるようですし、色々と不整合が起きそうです。(ここはちゃんと調査・検証しきれてないので歯切れの悪い記載で申し訳ありませんが。。)
そのため、ちょっと面倒ですが今回のやり方の方が確実かと思います。確認コードの取り方が特殊ですが、DBの更新自体は通常の方法と同じように行われているはずだからです。


おわりに

というわけで、Difyがメール送信できない状況でも、ユーザーのメールアドレスは更新できました。だいぶ特殊なシチュエーションのため実用性はないかもしれませんが、内部のロジックが少し理解できて勉強になりました。
Difyのコードは以下で公開されているので、KiroなどのAIエージェントにコードを解析させて、解説をマークダウンで書いてもらうと、かなり理解が捗りました。

github.com

あと、今回は検証した直後に、そのままの勢いでブログを書いてみました。日を空けてしまうと腰が重くなるし記憶も曖昧になって結局書かずじまいになってしまうことがあるので、検証直後に一気に書けたのは個人的に良かったです。そのせいで粗い部分もありますが、日の目を見ないよりはマシだと思うことにします。

今回のブログは以上です。あんまり参考にならないかもしれませんが、何か少しでも参考になることがあれば幸いです。

2026年2月の振り返り

早いもので、2026年もあっという間に2か月が過ぎてしまいました。

先月、2026年1月の振り返りを行ったので、同様に、2026年2月の振り返りを行いたいと思います。

インプット

以下の記事を参考に、CDKの基礎を学習しました。

zenn.dev

ただ、上記は必要に迫られてごく短期間だけやった感じで、1か月を通して一定のペースでインプットを行うことは出来ませんでした。

アウトプット

技術ブログ

2月は1つだけ技術ブログを書きました。

yuy-83.hatenablog.com

こちらのブログは個人的に以下の点で良かったと思っています。

  • 実際の困りごとに役立てることができた
  • 資格勉強で座学で学習したことを、実際に使ってみることができた
  • AIを上手く活用することができた

ただ、2月中に出来ればもう1つくらいブログを書きたかった所です。(3月1日にブログを書いたのですが、もう1日早く書いていれば。。)

LTや登壇

社内外どちらでもLTや登壇は行いませんでした。出来れば毎月1つくらいは何かしたいところですね。

運動

ジム

2月は計10日ジムに行っていました。
3日に1回くらいのペースは維持できているので、許容範囲かとは思いますが、これ以上日数が減らないようにキープしたいところです。

有酸素運動(ウォーキング)

アプリの記録によると、68.5㎞でした。
先月が47.5㎞だったので、20㎞くらい増えましたね。
3月は100㎞くらいにしたいところです。

ストレッチ

ストレッチは相変わらずやったりやらなかったりですね。。大体風呂上りにやるようにしているので、入浴時間が遅くなるとやらなくなる傾向があります。入浴時間を早めるのがカギかもしれません。

生活習慣

先月同様、前半は良かったのですが、後半に崩れてしまいました。。
先月は資格勉強で崩れましたが、今月は仕事でちょっとハマってしまい、そこで崩れてしまいました。。こればっかりはどうしようもない部分もありますが、区切りはしっかりつけて(深夜や明け方までやらない)、生活ペースを壊さないようにする必要がありますね。

その他

後半に生活習慣が崩れてからは、SNSやYouTubeをダラダラ見て過ごす時間が増えてしまいました。もっと読書とかに時間を使えば良かったなと思いました。

ただ、能動的に楽しむ趣味も新たに見つかって、それはホントに良かったです。まだ続くか分からないですが。。

まとめ

先月の振り返りで、「2月は安定したペースを維持することと、手を動かすこと(特に初動を早めること)を意識していきたいです。」とまとめていたのですが、正直、出来ていたとは言い難いですね。。

このままズルズルいかないように、3月は年度末をしっかり締めたいと思います。

送信元IPを制限したIAMユーザーでCloudformationのカスタムリソースをデプロイしたら失敗した話

タイトルが長くてすいません(挨拶)

タイトル通りなのですが、送信元IPアドレスを制限したIAMユーザーで、Cloudformationカスタムリソースをデプロイしたら失敗しました。
原因と解決方法について、あまり情報が出てこなかったので、事の顛末をブログに残そうと思います。

はじめに結論

はじめに結論をサマリで書くと、以下の通りです。

  • カスタムリソース用LambdaのzipファイルをS3に置いている場合に発生する
  • 送信元IPを制限しているIAMの権限でCloudformationを実行すると発生する
  • S3からのzipファイル取得に失敗する
  • 失敗する原因は、S3からzipファイルを取得する際の送信元IPが特殊な値となるため

以降で詳細について記載します。

事前準備

まずは事前準備として、以下のものを準備します。

  • 送信元IP制限がかかったIAMユーザー
  • カスタムリソース用のLambda関数
  • Lambda関数のzipファイルを格納するS3バケット
  • 上記S3バケットのデータイベントを記録するCloudTrail証跡
  • Cloudformationテンプレート

IAMユーザー

AdministratorAccess権限と、送信元IPを制限するカスタムポリシーをアタッチしたIAMユーザーを用意します。

Lambda関数

まず、カスタムリソース用のLambda関数を用意します。今回は、Cloudformationのイベント種別に応じたレスポンスを返すだけの関数を用意しました。

Lambda関数(クリック・タップで詳細表示)

import json
import urllib3
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)
http = urllib3.PoolManager()

def handler(event, context):
    logger.info(f"Request: {json.dumps(event)}")
    
    response_status = "SUCCESS"
    response_data = {}
    
    try:
        request_type = event['RequestType']
        
        if request_type == 'Create':
            logger.info("Creating custom resource")
            response_data['Message'] = "Resource created successfully"
            
        elif request_type == 'Update':
            logger.info("Updating custom resource")
            response_data['Message'] = "Resource updated successfully"
            
        elif request_type == 'Delete':
            logger.info("Deleting custom resource")
            response_data['Message'] = "Resource deleted successfully"
            
    except Exception as e:
        logger.error(f"Error: {str(e)}")
        response_status = "FAILED"
        response_data['Message'] = str(e)
    
    send_response(event, context, response_status, response_data)
    
def send_response(event, context, response_status, response_data):
    response_body = {
        'Status': response_status,
        'Reason': f"See CloudWatch Log Stream: {context.log_stream_name}",
        'PhysicalResourceId': context.log_stream_name,
        'StackId': event['StackId'],
        'RequestId': event['RequestId'],
        'LogicalResourceId': event['LogicalResourceId'],
        'Data': response_data
    }
    
    logger.info(f"Response: {json.dumps(response_body)}")
    
    encoded_body = json.dumps(response_body).encode('utf-8')
    
    http.request(
        'PUT',
        event['ResponseURL'],
        body=encoded_body,
        headers={'Content-Type': ''}
    )


次に、このLambda関数をzip化し、予め作っておいたS3バケットにアップロードしておきます。(S3バケットは全てデフォルト設定のため、作成過程は省略します)

CloudTrail証跡

S3からのzipファイル取得が失敗した時のログを確認するために、CloudTrail証跡でデータイベントの取得をオンにします。zipファイルを格納したS3のイベントだけ取れれば良いので、基本イベントセレクタでシンプルに設定します。

CloudFormationテンプレート

最後にCloudformationテンプレートです。上記のLambda関数とカスタムリソースを定義します。

Cloudformation(クリック・タップで詳細表示)

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation Custom Resource Sample

Resources:
  # IAM Role for Lambda
  CustomResourceLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  # Lambda Function
  CustomResourceLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: CustomResourceHandler
      Runtime: python3.12
      Handler: lambda_function.handler
      Role: !GetAtt CustomResourceLambdaRole.Arn
      Code:
        S3Bucket: "yamashita-test-xxxxx"
        S3Key: lambda_function.zip
      Timeout: 60

  # Custom Resource
  MyCustomResource:
    Type: Custom::MyCustomResource
    Properties:
      ServiceToken: !GetAtt CustomResourceLambda.Arn
      CustomProperty: SampleValue

Outputs:
  LambdaFunctionArn:
    Description: Lambda Function ARN
    Value: !GetAtt CustomResourceLambda.Arn
  
  CustomResourceMessage:
    Description: Message from Custom Resource
    Value: !GetAtt MyCustomResource.Message

Outputsで、カスタムリソースLambdaのメッセージ部分を出力するようにします。


以上で事前準備は完了です。

検証

ここから実際に検証してみます。

送信元IP制限がかかった状態で実行

送信元IP制限がかかった状態でCloudformationを実行すると失敗しました。メッセージを見ると、S3のGetObjectがAccessDenyされたと記載されています。

S3のGetObjectがポリシーによりDenyされたとのことだが…


しかし、マネコンには許可されたIPアドレスからログインしているので、最初は何故このようなメッセージが出るのか分かりませんでした。

CloudTrailのログを確認する

CloudTrailのログを確認します。ログはCloudWatch Logsに転送しているので、Insightsで適当に以下のように検索して対象のログを見つけました。

fields eventName
| sort @timestamp desc
| filter eventName like "Object" and eventSource = "s3.amazonaws.com"
| limit 200

すると以下のようなログが出てきました。(一部マスキングしてます)

CloudTrailログ(クリック・タップで詳細表示)

{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "IAMUser",
        "principalId": "AIDATXXXXXXXXXXXX",
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/SourceIpRestrictUser",
        "accountId": "xxxxxxxxxxxx",
        "accessKeyId": "ASIAXXXXXXXXXXXXX",
        "userName": "SourceIpRestrictUser",
        "sessionContext": {
            "attributes": {
                "creationDate": "2026-03-01T12:33:20Z",
                "mfaAuthenticated": "false"
            }
        },
        "invokedBy": "lambda.amazonaws.com"
    },
    "eventTime": "2026-03-01T12:35:50Z",
    "eventSource": "s3.amazonaws.com",
    "eventName": "GetObject",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "lambda.amazonaws.com",
    "userAgent": "lambda.amazonaws.com",
    "errorCode": "AccessDenied",
    "errorMessage": "User: arn:aws:iam::xxxxxxxxxxxx:user/SourceIpRestrictUser is not authorized to perform: s3:GetObject on resource: \"arn:aws:s3:::yamashita-test-xxxxx/lambda_function.zip\" with an explicit deny in an identity-based policy",
    "requestParameters": {
        "bucketName": "yamashita-test-xxxxx",
        "Host": "yamashita-test-xxxxx.s3.ap-northeast-1.amazonaws.com",
        "key": "lambda_function.zip"
    },
    "responseElements": null,
    "additionalEventData": {
        "SignatureVersion": "SigV4",
        "CipherSuite": "TLS_AES_128_GCM_SHA256",
        "bytesTransferredIn": 0,
        "AuthenticationMethod": "AuthHeader",
        "x-amz-id-2": "uZFWUrp0ESZLdT+qDn4mxeXh1sF0MoOuXeUMkqyDBLY66hqQgrnfGJ71RLg7Tij+GubtGns8sWs=",
        "bytesTransferredOut": 452
    },
    "requestID": "ZFV4M461MXVZFJ54",
    "eventID": "9089e4a1-4e27-397c-8937-164221cd5476",
    "readOnly": true,
    "resources": [
        {
            "accountId": "xxxxxxxxxxxx",
            "type": "AWS::S3::Bucket",
            "ARN": "arn:aws:s3:::yamashita-test-xxxxx"
        },
        {
            "type": "AWS::S3::Object",
            "ARN": "arn:aws:s3:::yamashita-test-xxxxx/lambda_function.zip"
        }
    ],
    "eventType": "AwsApiCall",
    "managementEvent": false,
    "recipientAccountId": "xxxxxxxxxxxx",
    "sharedEventID": "06b79d4a-126a-4649-95ae-1829f2381159",
    "vpcEndpointId": "lambda.amazonaws.com",
    "vpcEndpointAccountId": "lambda.amazonaws.com",
    "eventCategory": "Data"
}

注目すべきは6行目の "arn" と22行目の "sourceIPAddress" です。PrincipalのARNはIAMユーザーのものになっていますが、送信元IPは "lambda.amazonaws.com" となっています。結果的に、IAMユーザーの送信元IP制限に引っかかって、AccessDeniedとなってしまっています。

送信元IPを外して再試行

IAMユーザーから送信元IPを外して再度実施してみます。

AdministratorAccessだけ残った状態


再度Cloudformationを実行すると、今度は成功しました。


出力からメッセージも確認できました。また、CloudWatch LogsにもLambda関数のログが記録されていました。

Cloudformationの出力画面


Lambda関数のログ

CloudTrailのログを再度確認する

CloudTrailのログも再度確認します。

CloudTrailログ(クリック・タップで詳細表示)

{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "IAMUser",
        "principalId": "AIDATO53NFKULFCBSUVCW",
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/SourceIpRestrictUser",
        "accountId": "xxxxxxxxxxxx",
        "accessKeyId": "ASIATO53NFKUGTKABSOL",
        "userName": "SourceIpRestrictUser",
        "sessionContext": {
            "attributes": {
                "creationDate": "2026-03-01T12:33:20Z",
                "mfaAuthenticated": "false"
            }
        },
        "invokedBy": "lambda.amazonaws.com"
    },
    "eventTime": "2026-03-01T13:03:22Z",
    "eventSource": "s3.amazonaws.com",
    "eventName": "GetObject",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "lambda.amazonaws.com",
    "userAgent": "lambda.amazonaws.com",
    "requestParameters": {
        "bucketName": "yamashita-test-xxxxx",
        "Host": "yamashita-test-xxxxx.s3.ap-northeast-1.amazonaws.com",
        "key": "lambda_function.zip"
    },
    "responseElements": null,
    "additionalEventData": {
        "SignatureVersion": "SigV4",
        "CipherSuite": "TLS_AES_128_GCM_SHA256",
        "bytesTransferredIn": 0,
        "AuthenticationMethod": "AuthHeader",
        "x-amz-id-2": "tDLMSWfwVookfa13ZhdSL0pJ1wsikRzoU6kfn43ks6ZSeDbZtwfDB2oh1ZzFxiYvmfP1cJUmBJg=",
        "bytesTransferredOut": 791
    },
    "requestID": "TG329Y8X5ZAFPDZD",
    "eventID": "a3d8b0a2-0141-362d-93bf-1b5093c44e09",
    "readOnly": true,
    "resources": [
        {
            "accountId": "xxxxxxxxxxxx",
            "type": "AWS::S3::Bucket",
            "ARN": "arn:aws:s3:::yamashita-test-xxxxx"
        },
        {
            "type": "AWS::S3::Object",
            "ARN": "arn:aws:s3:::yamashita-test-xxxxx/lambda_function.zip"
        }
    ],
    "eventType": "AwsApiCall",
    "managementEvent": false,
    "recipientAccountId": "xxxxxxxxxxxx",
    "sharedEventID": "ccc5899e-1600-4e5d-af1c-dbf501714cf0",
    "vpcEndpointId": "lambda.amazonaws.com",
    "vpcEndpointAccountId": "lambda.amazonaws.com",
    "eventCategory": "Data"
}

Principalと送信元IPは同じですが、今度は成功しました。

補足というか推測

今回試していないのですが、送信元IP制限のかかっていないCloudformationのサービスロールを使えば、IAMユーザーに送信元IP制限がかかっていても、おそらくデプロイは成功したのではないかと思います。IAMユーザーの送信元IP制限を外せない状況の時は、そのような解決策になるのではないかと思います。

おわりに

以上、Cloudformationのカスタムリソースに関する検証でした。正直かなり今更感のあるネタな気がしますが、私自身つい最近ハマったので、せっかくなのでブログにしました。
いつかどこかで誰かの役に立てば幸いです。

Amazon Bedrockのバッチ推論を使って、大量のCVEの内容を一括で解説させてみた

Amazon Inspector で脆弱性の検知をすると、意外とたくさんの脆弱性が検知されますよね(挨拶)。
CVEの情報を載せてくれてはいるものの、NVD等の脆弱性データベースの説明文は難解で、正直私は読んだだけでは理解が出来ませんでした。。

そこで、こんな時こそAIの出番ということで、CVEの情報をもっと嚙み砕いて解説してもらうことにしました。
CVEの数が多いため、せっかくなので最近勉強したBedrockのバッチ推論を使って一括で解説させてみます。

謝辞

今回の検証をするにあたり、以下の記事を参考にさせていただきました。この場を借りてお礼を申し上げます。ありがとうございました。

dev.classmethod.jp

qiita.com

zenn.dev

事前準備

バッチ推論を実行する前に、いくつかの事前準備を行います。

事前準備1:CVEの脆弱性情報を取得するためにNVDのAPIキーを取得

NVDはAPIを公開しており、脆弱性情報を取得できます。まずは脆弱性情報を取得するためのAPIキーを取得します。
キーは以下のページから取得できます。

nvd.nist.gov

個人利用なので、Organization Nameは「individual」とし、Organizations Typeは「Personal Use」を選択しました。利用規約に同意してSubmitすると、登録したメールアドレスにAPIキー発行のためのリンクとUUIDが送られてきます。

リンクをクリックし、登録したメールアドレスと送られてきたUUIDを入力し「Confirm」ボタンを押すと、APIキーが発行されます。

事前準備2:バッチ推論のためのjsonlファイルを作成

バッチ推論のためのjsonlファイルを作成します。今回は、jsonlは以下のようなフォーマットにします。※実際には1行ですが、見やすいように改行しています。

record = {
    "recordId": # CVE-ID,
    "modelInput": {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 4096,
        "temperature": 0.1,
        "system": "あなたはセキュリティの専門家です。以下の脆弱性情報について解説してください。どのような場合に特にリスクが高まるのか、具体的にどのような被害が発生する可能性があるのか知りたいです。解説は、LPICやLinuCのレベル1~2くらいの知識がある人が理解できるような内容にしてください。",
        "messages": [
            {
                "role": "user",
                "content": # CVE-IDに応じた脆弱性情報 
            }
        ]
    }
}

recordIdはCVE-IDにします。contentに、各CVE-IDに応じた脆弱性情報を記載します。(NVDからAPIで取得します)
今回は、下記のようなCVE-IDのリストを用意し、リストに記載されたCVE-IDの脆弱性情報を取得して上記jsonlを作成するPythonスクリプトを準備します。

■cve_list.txt

CVE-2025-27363
CVE-2016-9843
CVE-2024-12084
・
・
・


jsonlファイルを作成するためのPythonスクリプトは以下です。

import requests
import json
import time

# --- 設定項目 ---
INPUT_FILE = "cve_list.txt"
OUTPUT_FILE = "cve_bedrock_input.jsonl"
API_KEY = "ここにAPIキーを入力"
BASE_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"

def get_cve_full_vulnerability(cve_id):
    headers = {"apiKey": API_KEY} if API_KEY else {}
    params = {"cveId": cve_id}
    
    try:
        response = requests.get(BASE_URL, headers=headers, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()

        vulnerabilities = data.get("vulnerabilities", [])
        if vulnerabilities:
            return vulnerabilities[0]
        return None # データが見つからない場合

    except Exception as e:
        print(f"Error fetching {cve_id}: {e}")
        return None

def main():
    with open(INPUT_FILE, "r") as f_in:
        cve_ids = [line.strip() for line in f_in if line.strip()]
    
    with open(OUTPUT_FILE, "w", encoding="utf-8") as f_out:
        for cve_id in cve_ids:
            print(f"Processing {cve_id}...")
            vulnerability_data = get_cve_full_vulnerability(cve_id)
            
            if vulnerability_data:
                # 指定されたフォーマットに構成
                record = {
                    "recordId": cve_id,
                    "modelInput": {
                        "anthropic_version": "bedrock-2023-05-31",
                        "max_tokens": 4096,
                        "temperature": 0.1,
                        "system": "あなたはセキュリティの専門家です。以下の脆弱性情報について解説してください。どのような場合に特にリスクが高まるのか、具体的にどのような被害が発生する可能性があるのか知りたいです。解説は、LPICやLinuCのレベル1~2くらいの知識がある人が理解できるような内容にしてください。",
                        "messages": [
                            {
                                "role": "user",
                                "content": json.dumps(vulnerability_data, ensure_ascii=False) # JSON文字列として埋め込み
                            }
                        ]
                    }
                }
                
                # JSONLとして1行書き出し
                f_out.write(json.dumps(record, ensure_ascii=False) + "\n")
                f_out.flush()
            
            # レート制限対策
            time.sleep(0.6 if API_KEY else 6.0)

    print(f"\n完了! {OUTPUT_FILE} を作成しました。")

if __name__ == "__main__":
    main()


スクリプトを実行すると以下のようなレコードが作成されます。(これで1件分です)

{"recordId": "CVE-2025-27363", "modelInput": {"anthropic_version": "bedrock-2023-05-31", "max_tokens": 4096, "temperature": 0.1, "system": "あなたはセキュリティの専門家です。以下の脆弱性情報について解説してください。どのような場合に特にリスクが高まるのか、具体的にどのような被害が発生する可能性があるのか知りたいです。解説は、LPICやLinuCのレベル1~2くらいの知識がある人が理解できるような内容にしてください。", "messages": [{"role": "user", "content": "{\"cve\": {\"id\": \"CVE-2025-27363\", \"sourceIdentifier\": \"cve-assign@fb.com\", \"published\": \"2025-03-11T14:15:25.427\", \"lastModified\": \"2025-10-27T17:06:41.997\", \"vulnStatus\": \"Analyzed\", \"cveTags\": [], \"descriptions\": [{\"lang\": \"en\", \"value\": \"An out of bounds write exists in FreeType versions 2.13.0 and below (newer versions of FreeType are not vulnerable) when attempting to parse font subglyph structures related to TrueType GX and variable font files. The vulnerable code assigns a signed short value to an unsigned long and then adds a static value causing it to wrap around and allocate too small of a heap buffer. The code then writes up to 6 signed long integers out of bounds relative to this buffer. This may result in arbitrary code execution. This vulnerability may have been exploited in the wild.\"}, {\"lang\": \"es\", \"value\": \"Existe una escritura fuera de los límites en las versiones 2.13.0 y anteriores de FreeType al intentar analizar estructuras de subglifos de fuentes relacionadas con archivos de fuentes TrueType GX y variables. El código vulnerable asigna un valor short con signo a un long sin signo y luego añade un valor estático, lo que provoca un bucle y asigna un búfer de montón demasiado pequeño. El código escribe entonces hasta 6 enteros long con signo fuera de los límites en relación con este búfer. Esto puede provocar la ejecución de código arbitrario. Esta vulnerabilidad podría haber sido explotada in situ.\"}], \"metrics\": {\"cvssMetricV31\": [{\"source\": \"cve-assign@fb.com\", \"type\": \"Secondary\", \"cvssData\": {\"version\": \"3.1\", \"vectorString\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\", \"baseScore\": 8.1, \"baseSeverity\": \"HIGH\", \"attackVector\": \"NETWORK\", \"attackComplexity\": \"HIGH\", \"privilegesRequired\": \"NONE\", \"userInteraction\": \"NONE\", \"scope\": \"UNCHANGED\", \"confidentialityImpact\": \"HIGH\", \"integrityImpact\": \"HIGH\", \"availabilityImpact\": \"HIGH\"}, \"exploitabilityScore\": 2.2, \"impactScore\": 5.9}, {\"source\": \"nvd@nist.gov\", \"type\": \"Primary\", \"cvssData\": {\"version\": \"3.1\", \"vectorString\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\", \"baseScore\": 8.1, \"baseSeverity\": \"HIGH\", \"attackVector\": \"NETWORK\", \"attackComplexity\": \"HIGH\", \"privilegesRequired\": \"NONE\", \"userInteraction\": \"NONE\", \"scope\": \"UNCHANGED\", \"confidentialityImpact\": \"HIGH\", \"integrityImpact\": \"HIGH\", \"availabilityImpact\": \"HIGH\"}, \"exploitabilityScore\": 2.2, \"impactScore\": 5.9}]}, \"cisaExploitAdd\": \"2025-05-06\", \"cisaActionDue\": \"2025-05-27\", \"cisaRequiredAction\": \"Apply mitigations per vendor instructions, follow applicable BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.\", \"cisaVulnerabilityName\": \"FreeType Out-of-Bounds Write Vulnerability\", \"weaknesses\": [{\"source\": \"134c704f-9b21-4f2e-91b3-4a467353bcc0\", \"type\": \"Secondary\", \"description\": [{\"lang\": \"en\", \"value\": \"CWE-787\"}]}], \"configurations\": [{\"nodes\": [{\"operator\": \"OR\", \"negate\": false, \"cpeMatch\": [{\"vulnerable\": true, \"criteria\": \"cpe:2.3:a:freetype:freetype:*:*:*:*:*:*:*:*\", \"versionEndIncluding\": \"2.13.0\", \"matchCriteriaId\": \"47088474-E5B5-4220-8F12-D664F2DED5C1\"}]}]}, {\"nodes\": [{\"operator\": \"OR\", \"negate\": false, \"cpeMatch\": [{\"vulnerable\": true, \"criteria\": \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\", \"matchCriteriaId\": \"FA6FEEC2-9F11-4643-8827-749718254FED\"}]}]}], \"references\": [{\"url\": \"https://www.facebook.com/security/advisories/cve-2025-27363\", \"source\": \"cve-assign@fb.com\", \"tags\": [\"Third Party Advisory\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/13/1\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/13/11\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/13/12\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/13/2\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/13/3\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/13/8\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/14/1\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/14/2\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/14/3\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/03/14/4\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/05/06/3\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"https://lists.debian.org/debian-lts-announce/2025/03/msg00030.html\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"https://source.android.com/docs/security/bulletin/2025-05-01\", \"source\": \"134c704f-9b21-4f2e-91b3-4a467353bcc0\", \"tags\": [\"Third Party Advisory\"]}, {\"url\": \"https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2025-27363\", \"source\": \"134c704f-9b21-4f2e-91b3-4a467353bcc0\", \"tags\": [\"US Government Resource\"]}]}}"}]}}

事前準備3:バッチ推論用のS3とIAMロールを用意する

バッチ推論用のIAMロールを用意します。信頼ポリシーは以下です。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Service": "bedrock.amazonaws.com"
    },
    "Action": "sts:AssumeRole"
  }]
}


IAMポリシーは以下です。今回はインプットもアウトプットも同じS3に格納するため、バケットは一つだけです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::xxxxx-bedrock-batch-bucket",
        "arn:aws:s3:::xxxxx-bedrock-batch-bucket/*"
      ] 
    },
    {
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel"
      ],
      "Resource": "*"
    }
  ]
}


続いて、事前準備2で作成したjsonlをS3にコピーします。

$ aws s3 cp ./cve_bedrock_input.jsonl s3://xxxxx-bedrock-batch-bucket/input/
upload: ./cve_bedrock_input.jsonl to s3://xxxxx-bedrock-batch-bucket/input/cve_bedrock_input.jsonl
$ 

事前準備4:バッチ推論ジョブ投入スクリプトを準備する

今回、せっかくなのでバッチ推論もスクリプトで投入してみます。以下のPythonスクリプトを使用します。
リージョンは東京リージョンで、モデルはClaude Sonnet 4.5にしてみます。クロスリージョン推論に対応しているので、モデルIDの頭にjpを付けています。詳細は以下URLを参照ください。

docs.aws.amazon.com

import boto3

bedrock = boto3.client('bedrock', region_name='ap-northeast-1')

job_name = 'cve-explanation-batch-v3'

response = bedrock.create_model_invocation_job(
    jobName=job_name,
    roleArn='arn:aws:iam::{aws-account-id}:role/BedrockBatchInferenceRole',
    modelId='jp.anthropic.claude-sonnet-4-5-20250929-v1:0',
    inputDataConfig={
        's3InputDataConfig': {
            's3Uri': 's3://xxxxx-bedrock-batch-bucket/input/cve_bedrock_input.jsonl'
        }
    },
    outputDataConfig={
        's3OutputDataConfig': {
            's3Uri': 's3://xxxxxx-bedrock-batch-bucket/output/'
        }
    }
)

job_arn = response['jobArn']
job_id = job_arn.split('/')[-1]

print(f"✓ バッチジョブ投入完了")
print(f"ジョブ名: {job_name}")
print(f"ジョブID: {job_id}")
print(f"ARN: {job_arn}")

バッチ推論を実行してみる

それではいよいよバッチを実行してみます。

$ uv run run_batch_job.py 
Traceback (most recent call last):
  (一部省略)
botocore.errorfactory.AccessDeniedException: An error occurred (AccessDeniedException) when calling the CreateModelInvocationJob operation: Your account currently does not have access to this model. Model access setup is in progress. Please try again in 2 minutes.

と思ったらエラーが出てしまいました。。。Claude Sonnet 4.5を使うのが初めてだったので、モデルアクセスの有効化処理が走ったようです。少し待ってから再度スクリプトを実行してみます。

$ uv run run_batch_job.py 
✓ バッチジョブ投入完了
ジョブ名: cve-explanation-batch-v3
ジョブID: jil3xdpj2tc5
ARN: arn:aws:bedrock:ap-northeast-1:xxxxxxxxxxxx:model-invocation-job/jil3xdpj2tc5
$

投入が完了したようです。マネジメントコンソールのバッチ推論の画面を見てみます。

マネジメントコンソールのバッチ推論の画面

バッチ推論が検証中となっています。ちなみに2つ失敗しているものがありますが、これはBedrockサービスロールの権限不足で失敗したものです。

その後ステータスが「進行中」を経て「完了済み」になりました。


S3を見てみると、推論結果のファイルが格納されていました。

上のファイルが実際に推論した内容、下はバッチ推論の統計情報


ファイルをダウンロードし、拡張子のoutを外して中身を見てみます。まずは「manifest.json.out」からです。こちらはバッチ推論の統計情報が記載されています。具体的には、処理したレコード件数や入力・出力トークン数などが記載されています。今回は、178件のレコードを処理して全て成功したようです。

■manifest.json.out

{"totalRecordCount":178,"processedRecordCount":178,"successRecordCount":178,"errorRecordCount":0,"inputTokenCount":390271,"outputTokenCount":308208,"inputAudioSecond":0,"inputVideoSecond":0,"inputStandardImageCount":0,"inputDocumentImageCount":0,"inputTextTokenCount":0,"inputImageTokenCount":0,"inputAudioTokenCount":0,"inputVideoTokenCount":0,"outputTextTokenCount":0,"outputImageTokenCount":0}


続いて、「cve_bedrock_input.jsonl.out」です。こちらにはBedrockが実際に推論した結果が記載されています。量が膨大なので最初の1行だけ載せます。

■cve_bedrock_input.jsonl.out

{"modelInput":{"anthropic_version":"bedrock-2023-05-31","max_tokens":4096,"temperature":0.1,"system":"あなたはセキュリティの専門家です。以下の脆弱性情報について解説してください。どのような場合に特にリスクが高まるのか、具体的にどのような被害が発生する可能性があるのか知りたいです。解説は、LPICやLinuCのレベル1~2くらいの知識がある人が理解できるような内容にしてください。","messages":[{"role":"user","content":"{\"cve\": {\"id\": \"CVE-2025-4802\", \"sourceIdentifier\": \"3ff69d7a-14f2-4f67-a097-88dee7810d18\", \"published\": \"2025-05-16T20:15:22.280\", \"lastModified\": \"2025-11-03T20:19:11.153\", \"vulnStatus\": \"Modified\", \"cveTags\": [], \"descriptions\": [{\"lang\": \"en\", \"value\": \"Untrusted LD_LIBRARY_PATH environment variable vulnerability in the GNU C Library version 2.27 to 2.38 allows attacker controlled loading of dynamically shared library in statically compiled setuid binaries that call dlopen (including internal dlopen calls after setlocale or calls to NSS functions such as getaddrinfo).\"}, {\"lang\": \"es\", \"value\": \"La vulnerabilidad de la variable de entorno no confiable LD_LIBRARY_PATH en GNU C Library versión 2.27 a 2.38 permite al atacante cargar, controlada por un atacante, una librería compartida dinámicamente en binarios setuid compilados estáticamente que llaman a dlopen (incluidas las llamadas internas a dlopen después de setlocale o las llamadas a funciones NSS como getaddrinfo).\"}], \"metrics\": {\"cvssMetricV31\": [{\"source\": \"134c704f-9b21-4f2e-91b3-4a467353bcc0\", \"type\": \"Secondary\", \"cvssData\": {\"version\": \"3.1\", \"vectorString\": \"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\", \"baseScore\": 7.8, \"baseSeverity\": \"HIGH\", \"attackVector\": \"LOCAL\", \"attackComplexity\": \"LOW\", \"privilegesRequired\": \"NONE\", \"userInteraction\": \"REQUIRED\", \"scope\": \"UNCHANGED\", \"confidentialityImpact\": \"HIGH\", \"integrityImpact\": \"HIGH\", \"availabilityImpact\": \"HIGH\"}, \"exploitabilityScore\": 1.8, \"impactScore\": 5.9}]}, \"weaknesses\": [{\"source\": \"3ff69d7a-14f2-4f67-a097-88dee7810d18\", \"type\": \"Secondary\", \"description\": [{\"lang\": \"en\", \"value\": \"CWE-426\"}]}], \"configurations\": [{\"nodes\": [{\"operator\": \"OR\", \"negate\": false, \"cpeMatch\": [{\"vulnerable\": true, \"criteria\": \"cpe:2.3:a:gnu:glibc:*:*:*:*:*:*:*:*\", \"versionStartIncluding\": \"2.27\", \"versionEndIncluding\": \"2.38\", \"matchCriteriaId\": \"29CCC9F6-2130-4DA8-8B5D-7A00337CBC0A\"}]}]}], \"references\": [{\"url\": \"https://sourceware.org/bugzilla/show_bug.cgi?id=32976\", \"source\": \"3ff69d7a-14f2-4f67-a097-88dee7810d18\", \"tags\": [\"Issue Tracking\"]}, {\"url\": \"https://sourceware.org/cgit/glibc/commit/?id=1e18586c5820e329f741d5c710275e165581380e\", \"source\": \"3ff69d7a-14f2-4f67-a097-88dee7810d18\", \"tags\": [\"Patch\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/05/16/7\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Mailing List\"]}, {\"url\": \"http://www.openwall.com/lists/oss-security/2025/05/17/2\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\", \"tags\": [\"Exploit\", \"Mailing List\"]}, {\"url\": \"https://lists.debian.org/debian-lts-announce/2025/05/msg00033.html\", \"source\": \"af854a3a-2127-422b-91ae-364da2661108\"}]}}"}]},"modelOutput":{"model":"claude-sonnet-4-5-20250929","id":"msg_bdrk_01EYLYU15eyApF5L9kxzhZ66","type":"message","role":"assistant","content":[{"type":"text","text":"# CVE-2025-4802 の解説\n\n## 脆弱性の概要\n\nこの脆弱性は、GNU C Library (glibc) バージョン 2.27 から 2.38 において、**LD_LIBRARY_PATH環境変数が不適切に信頼されてしまう**問題です。\n\n通常、setuidビットが設定されたプログラムでは、セキュリティ上の理由からLD_LIBRARY_PATHは無視されるべきですが、**静的にコンパイルされたsetuidバイナリ**が特定の条件下でdlopenを呼び出す場合、この保護が機能しません。\n\n## 技術的な詳細\n\n### 影響を受ける条件\n\n以下の**すべての条件**を満たす場合に脆弱性が発現します:\n\n1. **静的にコンパイルされた**setuidバイナリである\n2. そのバイナリが以下のいずれかを実行する:\n   - `dlopen()`を直接呼び出す\n   - `setlocale()`を呼び出す(内部でdlopenが呼ばれる)\n   - NSS関数(`getaddrinfo()`など)を呼び出す\n\n### 攻撃シナリオ\n\n```bash\n# 攻撃者が悪意のある共有ライブラリを作成\n$ cat > evil.c << EOF\n#include <stdio.h>\nvoid __attribute__((constructor)) init() {\n    system(\"/bin/sh\");  // root権限でシェルを起動\n}\nEOF\n\n$ gcc -shared -fPIC evil.c -o /tmp/evil.so\n\n# LD_LIBRARY_PATHを設定して脆弱なsetuidプログラムを実行\n$ LD_LIBRARY_PATH=/tmp ./vulnerable_setuid_program\n# → root権限のシェルが起動される\n```\n\n## リスクが高まる状況\n\n### 1. **カスタムビルドされたシステムユーティリティ**\n- 組織内で独自にコンパイルされたsetuidツール\n- 静的リンクでビルドされたセキュリティツール\n\n### 2. **組み込みシステム**\n- 依存関係を減らすために静的リンクが好まれる環境\n- BusyBoxなどの軽量ユーティリティ\n\n### 3. **コンテナ環境**\n- 最小限のイメージを作るために静的バイナリを使用している場合\n\n## 想定される被害\n\n### 深刻度: **HIGH (CVSS 7.8)**\n\n1. **権限昇格**\n   - 一般ユーザーがroot権限を取得\n   - システム全体の制御を奪取\n\n2. **機密情報の窃取**\n   - `/etc/shadow`などの保護されたファイルへのアクセス\n   - 他ユーザーのデータへの不正アクセス\n\n3. **システムの改ざん**\n   - バックドアの設置\n   - システムファイルの改変\n   - マルウェアのインストール\n\n## 対策\n\n### 即座に実施すべき対策\n\n1. **glibcのアップデート**\n   ```bash\n   # Debian/Ubuntu\n   sudo apt update && sudo apt upgrade libc6\n   \n   # RHEL/CentOS\n   sudo yum update glibc\n   ```\n\n2. **脆弱なバイナリの特定**\n   ```bash\n   # setuidビットが設定された静的バイナリを検索\n   find / -perm -4000 -type f -exec file {} \\; | grep \"statically linked\"\n   ```\n\n3. **一時的な緩和策**\n   - 不要なsetuidビットの削除\n   ```bash\n   sudo chmod u-s /path/to/suspicious/binary\n   ```\n\n### 長期的な対策\n\n- 可能な限り**動的リンク**を使用する\n- setuidプログラムの使用を最小限に抑える\n- capabilities機能の活用を検討\n\n## 補足: なぜ静的バイナリで問題が起きるのか\n\n- **動的リンクバイナリ**: 起動時にリンカー(ld.so)がセキュリティチェックを行い、setuidプログラムではLD_LIBRARY_PATHを無視\n- **静的リンクバイナリ**: リンカーを経由しないため、dlopen実行時にこのチェックが抜け落ちる\n\nこの脆弱性は、セキュリティ機構の「想定外の組み合わせ」によって生じた典型的な例と言えます。"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1191,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1299}},"recordId":"CVE-2025-4802"}

推論結果だけでなく、入力情報やレコード毎の統計情報も記載されていますね。これだと見づらいので、推論結果だけを抽出したい場合は、もうひと手間かける必要がありそうです。

バッチ推論の結果を見やすくする

というわけで、追加のスクリプトを作ってバッチ推論の結果を見やすくします。jsonlから「modelOutput -> content -> text」 の順に掘り進んで、結果を抽出します。マークダウン形式で回答を生成したようなので、マークダウン形式のファイルで保存します。

まず、先ほどの「cve_bedrock_input.jsonl.out」を「cve_bedrock_output.jsonl」というファイル名に変更します。そのうえで、以下のスクリプトを実行します。

import json

INPUT_BATCH_FILE = "cve_bedrock_output.jsonl"  # Bedrockからの推論結果が保存されたファイル
OUTPUT_MARKDOWN_FILE = "cve_format.md"    # 読みやすく整形した保存先

def format_bedrock_output(input_path, output_path):
    with open(input_path, "r", encoding="utf-8") as f_in, \
         open(output_path, "w", encoding="utf-8") as f_out:
        
        for line in f_in:
            if not line.strip():
                continue
            
            try:
                data = json.loads(line)
                
                # recordId (CVE-ID) の取得
                cve_id = data.get("recordId", "Unknown ID")
                
                # modelOutput -> content -> text の順に掘り進む
                # contentはリスト形式で返ってくるため [0] を指定
                contents = data.get("modelOutput", {}).get("content", [])
                
                if contents:
                    # テキスト部分を抽出
                    explanation = contents[0].get("text", "解説が見つかりませんでした。")

                    # 画面表示用の整形
                    header = f"{'='*20} {cve_id} {'='*20}"
                    print(header)
                    print(explanation)
                    print("\n")
                    
                    # ファイル保存用の書き出し
                    f_out.write(header + "\n")
                    f_out.write(explanation + "\n\n")
                    
            except Exception as e:
                print(f"エラーが発生しました: {e}")

if __name__ == "__main__":
    format_bedrock_output(INPUT_BATCH_FILE, OUTPUT_MARKDOWN_FILE)
    print(f"整形完了。詳細は {OUTPUT_MARKDOWN_FILE} を確認してください。")


スクリプトを実行したところ、マークダウンのファイルに整形結果が追記されていました。

かなり大ボリュームになってしまった

かなり見やすくなりましたが、2万5,000行超えてますねコレ。。。ファイルサイズも850KBくらいありました。これはこれで読むのが大変。。。w
ただ、脆弱性の概要・リスクが高まる状況・想定される被害・対策など、かなり多岐に渡って解説されていますし、前提となる基礎知識の解説も織り交ぜられていて結構いい感じです。
ひとまず、やりたかった事は最低限は出来たかと思います。

おわりに

以上、Bedrockのバッチ推論の検証でした。

今回はCVEのリストを手動で用意していたり、Pythonスクリプトをローカル環境でいくつも手動実行していたりと、全体的にあまり洗練されていませんが、とりあえずBedrockのバッチ推論を使ってみたくて始めた検証なので、ご容赦いただければ幸いです。

実際には、Inspectorの脆弱性情報をAIに解説させたいというユースケースには、バッチ推論より適切なソリューションがあるかもしれません。機会があれば別の方法にもチャレンジしてみたいと思います。(それ以前に、脆弱性を放置せずに事前に潰すのが一番理想なのですが、、、)

今回のブログは以上です。何か少しでも参考になることがあれば幸いです。

2026年1月の振り返り

早いもので、2026年もあっという間に1か月が過ぎてしまいました。

昨年末に2025年の振り返りを行いましたが、もう少しこまめに振り返りをした方が良いのではないかと思ったので、今年はまず最初の1か月を振り返りたいと思います。

インプット

1月の前半は、以下の書籍でOAuthの基礎を学習しました。

nextpublishing.jp

私はまさにタイトルの通り雰囲気でOAuthを使っていたので、大変勉強になりました。ここで学習した内容は、何らかの形でアウトプットしたいです。

1月後半は、AWS Certified Generative AI Developer - Professional の取得のため、AWS Skill Builderの「Exam Prep Plan: AWS Certified Generative AI Developer - Professional (AIP-C01 - English)」というコースを学習していました。
その甲斐もあって、何とかGenerative AI Developer - Professionalを取得することが出来ました。(スコアはかなりギリギリでしたが。。)

インプットについては、継続して実施できたのではないかと思います。

アウトプット

技術ブログ

1月は全く技術ブログを書きませんでした。。月に2個くらいはコンスタントに書きたいと思っていたのですが、反省が必要ですね。

LTや登壇

外部向けの登壇はしていませんが、自社向けのLTを2回ほど実施しました。こちらは十分できたかなと思います。

昨年末の振り返りでも書いたのですが、アウトプットは上手く仕事やインプットと絡めれば、もっと効率化できると思います。
LTは業務で必要なインプットと上手く絡めることが出来たので、それをさらに技術ブログに転用できれば良かったのですが。。
もう一歩足りなかったな、という気持ちです。

アウトプットは「ちゃんとした内容にしなきゃ」みたいな気持ちが先行して腰が重くなりがちなので、「あくまで自分用のメモ、万が一誰かの役に立ったら超ラッキー」くらいの気持ちでやろうと思いました。

運動

去年は健康・体力の重要性を痛感したので、運動や生活習慣についてもしっかり振り返りたいと思います。(なんなら仕事や勉強以上に)

ジム

ジムの入館記録によると、1月は計14日ジムに行っていました。
回数だけでなく、あまり間を空けすぎずにコンスタントに行っていたことが良かったと思いました。
今年はとにかく年間を通してコンスタントに運動したいので、1回の強度よりも、ケガなく頻度よくを最優先にしようと思います。

有酸素運動(ウォーキング)

アプリの記録によると、14アクティビティで、距離は50㎞弱でした。
ちょっと少ないですね。。倍くらいにはしたい所です。

ストレッチ

私は非常に身体が硬く、それによって色々な不調を抱えているのですが、ストレッチがなかなか習慣化しません。。
1月前半は続けられていて、身体の調子も良かったのですが、後半はサボり気味になってしまいました。。

生活習慣

1月の前半は良かったのですが、後半は毎日夜更かしして非常に不規則な生活になってしまいました。。



運動、生活習慣ともに、資格取得の準備を始めてから一気に崩れてしまいました。多少無理してでも短期間で何とかしたいと思ってしまい、結果、生活習慣の乱れを招いてしまいました。
2月以降はコツコツと安定したペースで動きたいと思います。

その他

本を読んだりゲームをしたり、余暇の時間も多少は上手く過ごせたかなと思います。
人と直接会って話す機会もコンスタントにあったのも良かったです。

まとめ

全体的に前半は良かったのですが、後半は資格を短期間で取ろうと欲を出したら生活習慣が乱れて、やや身体の不調を招いてしまいました。
2月は安定したペースを維持することと、手を動かすこと(特に初動を早めること)を意識していきたいです。