はじめに
こんにちは!AWSを学習中のrikutoです。
先日、ECS FargateにデプロイしたGoアプリがRedis(ElastiCache)に接続できず、起動直後にクラッシュを繰り返す問題に遭遇しました。
ローカルのDockerでは問題なく動いていたのに、AWS上でだけ i/o timeout になるという状況です。
そこで今回は、自分自身の備忘録も兼ねて、
- なぜこのエラーが起きたのか(原因はElastiCache Serverless(Redis OSS)の仕様でした)
- どのように原因を切り分けて、解決したのか
ということを整理してまとめてみました。
この記事は、次のような方に向けています。
- GoアプリケーションをECS Fargateなどにデプロイして、ElastiCacheに接続したい方
- ローカル(Docker)ではRedisに接続できるのに、AWS上でだけタイムアウトする方
同じようなポイントで詰まっている方の参考になれば幸いです。
もし内容に誤りや、もっと良い方法があれば、ぜひコメントで教えていただけると嬉しいです!
記事の結論
ElastiCache Serverless(Redis OSS)は転送時の暗号化(TLS)が強制的に有効になっています。
そのため、TLSなしで接続しようとすると応答が返らず、i/o timeout になります。
Goの接続コード(infrastructure/redis.go)に、接続先がAWSのときだけTLS設定を渡すよう以下の修正を加えることで解決しました。
package infrastructure
import (
"context"
+ "crypto/tls"
+ "strings"
"github.com/iwariku/golang_twitter/utils"
"github.com/redis/go-redis/v9"
)
func NewRedisClient() *redis.Client {
redisAddress := utils.GetEnvOrDefault("REDIS_ADDRESS", "redis_server:6379")
+ // アドレスをベースに、AWS環境(TLS必須)かどうかの判定を行う
+ var tlsConfig *tls.Config
+ if strings.Contains(redisAddress, "amazonaws.com") {
+ tlsConfig = &tls.Config{
+ InsecureSkipVerify: true, // 学習・デバッグ環境のためスキップ
+ }
+ }
+
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddress,
Password: "",
DB: 0,
+ TLSConfig: tlsConfig, // ローカル時はnil(TLS無効)、AWS時は設定済みの値が入る
})
ctx := context.Background()
if err := redisClient.Ping(ctx).Err(); err != nil {
panic("Redisへの接続に失敗しました: " + err.Error())
}
return redisClient
}
なぜこの修正で直るのか、どうやって原因を絞り込んだのかを、以下で順番に説明していきます。
実行環境
| Tool | Version |
|---|---|
| Go | 1.25.5 |
| go-redis(github.com/redis/go-redis/v9) | v9.17.3 |
| Amazon ElastiCache Serverless(Redis OSS) | 7.1 |
| ローカルRedis(Docker) | redis:latest |
| AWS ECS on Fargate | - |
発生したエラー
ECS FargateにGoアプリケーションをデプロイしたところ、コンテナ起動直後に以下のパニックが発生しました。
タスクは起動とクラッシュ(強制終了)を繰り返す状態でした。
panic: Redisへの接続に失敗しました: dial tcp 10.0.151.216:6379: i/o timeout
なお、ローカルのDocker環境(TLSなしのRedis)では同じコードで問題なく接続できていました。
試したこと:セキュリティグループの切り分け
最初は「セキュリティグループが通信を遮断しているのでは」と疑いました。
そこで原因を切り分けるため、次の2つの仮説を立てて検証しました。
- A案: セキュリティグループで通信が遮断されている(ネットワークの問題)
- B案: 通信は届いているが、暗号化などの仕様不一致でRedis側が応答しない(コードの問題)
検証として、Redis側セキュリティグループのインバウンドルールを一時的に変更しました。
ポート 6379 のソースを、Fargate用セキュリティグループからの許可のみ → 0.0.0.0/0(全開放)に変えて再デプロイしました。
結果は、全開放しても全く同じ i/o timeout でした。
これでA案(ネットワーク)が消え、B案(コード側の仕様不一致)に原因を絞り込めました。
なお、検証後はセキュリティグループを必ず元の状態(特定のセキュリティグループからのみ許可)に戻してください。
原因:ElastiCache Serverless(Redis OSS)はTLSが強制有効
AWSの公式ドキュメントを確認したところ、ElastiCache Serverless(Redis OSS)では転送時の暗号化(TLS)が最初から強制的に有効になっていることが分かりました。
これはAWSの仕様であり、ユーザー側で無効にすることはできません。
一方、修正前のGoコードは TLSConfig を設定しておらず、暗号化なしの通常のTCPで接続していました。
TLSを要求するRedis側は暗号化されていない通信に応答を返さないため、アプリ側は接続を待ち続けます。
その結果、エラーは「接続拒否(connection refused)」ではなく i/o timeout として現れます。
解決方法
手順1: 接続先アドレスからTLSの要否を判定する
環境変数 REDIS_ADDRESS の値に amazonaws.com が含まれるかどうかで、AWS環境かを判定します。
AWS環境のときだけ *tls.Config を組み立て、ローカルのときは nil のままにします。
var tlsConfig *tls.Config
if strings.Contains(redisAddress, "amazonaws.com") {
tlsConfig = &tls.Config{
InsecureSkipVerify: true, // 学習・デバッグ環境のためスキップ
}
}
手順2: go-redisのOptionsにTLSConfigを渡す
redis.Options の TLSConfig フィールドに、手順1で組み立てた値を渡します。
nil を渡した場合はTLSなしで接続されるため、ローカルと本番を同じコードで扱えます。
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddress,
Password: "",
DB: 0,
TLSConfig: tlsConfig, // ローカル時はnil(TLS無効)、AWS時は設定済みの値が入る
})
補足: InsecureSkipVerify: true について
InsecureSkipVerify: true は、サーバー証明書の検証をスキップする設定です。
そのまま本番運用へコピペしないよう、以下の点に注意してください。
-
なぜ必要か: Goコンテナ内にAWSのルート証明書がないと、証明書検証に失敗して
x509関連のエラーで起動できないため - 今回許容した理由: RedisはPrivate Subnet内に配置しており、同じVPC内の経路でしか通信が発生しないため
-
本番でのベストプラクティス:
falseにした上で、コンテナ内にCA証明書(ca-certificatesパッケージ等)を配置して正しく検証させる
修正後、コードをビルドしてECRにプッシュし、ECSサービスで「新しいデプロイの強制」を実行したところ、タスクのステータスが RUNNING で安定し、起動時のパニックが解消されました。
まとめ
改めて、今回のポイントを振り返ります。
- ElastiCache Serverless(Redis OSS)はTLSが強制有効のため、TLSなしの接続は
i/o timeoutになる - go-redisでは
Options.TLSConfigに*tls.Configを渡すだけでTLS接続できる - 接続先アドレスで判定すれば、ローカル(TLSなし)とAWS(TLSあり)を同じコードで両立できる
-
i/o timeoutはネットワーク起因とは限らない。全開放テストで白黒つけると原因を絞り込める
最後に
最後まで読んでいただきありがとうございました!
「ローカルでは動くのにAWSでは動かない」系のエラーは原因の候補が多く、切り分けの進め方が大事だと実感しました。
この記事が同じエラーで悩んでいる方の助けになれば嬉しいです。
もし内容の誤りや、より良い方法があれば、ぜひコメントで教えてください!