こんにちは、タイミーSREチームの宮城です。 今回は弊社が Redash をFargateで構築/運用している話を紹介します。 背景 タイミーでは、CSやセールスのKPI策定から毎月の事業数値に至るまで、Redashが様々な用途で活用されています。 Fargateで構築する以前はEC2上のdocker-composeで運用されていましたが、以下の課題がありました。 オートスケールできないため、クエリが詰まってCPUが100%になってサービスが停止する。 その度slack上から再起動していた セットアップしたエンジニアが退社しており、インフラ構成図やノウハウの共有、IaCによる管理ができていない。 クエリや ダッシュ ボードなどのデータの定期的なバックアップができていない。 v7系からv8系へのアップデートがしたいが、アップデートによる影響範囲がわからず恐怖感がある。 事業に大きく関わるサービスなのにも関わらず、モニタリングやアラートができていない。 上記をFargateに移行することで解決することができました。 移行後の アーキテクチャ Redashで利用する ミドルウェア に関しては下記 コンポーネント を使い、全てをterraformで管理しています。 - PostgreSQL -> RDS - Redis -> ElastiCache それぞれの構成の紹介 ここからは、それぞれの構成をTerraformの ソースコード やタスク定義の JSON などを交えつつ説明していきます。 RDS/Elasticache ダッシュ ボードなどのデータが定期的なバックアップが行われていない問題は、RDSでsnapshotを取得することで解決しました。 それぞれ一番小さい インスタンス タイプのシングルAZ構成で構築しています。 実際に運用してみて負荷が大きければスペックを上げるつもりでしたが、現状問題なく捌けています。 将来、可用性を高めるためマルチAZにすることも容易であり、こういった柔軟なサーバーリソースの活用も クラウド の利点といえるでしょう。 RDSのTerraform privateサブネットに置いたシンプルな構成です。 applyが完了したらrootユーザーのパスワードを AWS コンソール上から変更し、接続情報をSecretsManagerに保管しています。 resource "aws_db_subnet_group" "redash" { name = "redash" subnet_ids = [ data.aws_subnet.private-subnet-1a.id, data.aws_subnet.private-subnet-1c.id, data.aws_subnet.private-subnet-1d.id, ] } /* Postgresの5432ポートを開くセキュリティグループ */ resource "aws_security_group" "rds-redash" { name = "rds-postgres-redash" description = "for redash" vpc_id = data.aws_vpc.vpc.id } resource "aws_security_group_rule" "rds-redash-ingress" { type = "ingress" from_port = 5432 to_port = 5432 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.rds-redash.id } resource "aws_security_group_rule" "rds-redash-egress" { type = "egress" from_port = 0 to_port = 0 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.rds-redash.id } /* defaultはよくないので追加。必要があればパラメータを増やしていく */ resource "aws_db_parameter_group" "redash" { name = "redash" family = "aurora-postgresql11" parameter { name = "log_min_duration_statement" value = "100" } } resource "aws_rds_cluster_parameter_group" "redash" { name = "redash" family = "aurora-postgresql11" parameter { name = "log_min_duration_statement" value = "100" } } /* DBクラスター */ resource "aws_rds_cluster" "redash" { cluster_identifier = "redash" engine = "aurora-postgresql" engine_version = "11.6" master_username = "postgres" master_password = "password" // 仮の値 backup_retention_period = 5 preferred_backup_window = "07:00-09:00" db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.redash.name db_subnet_group_name = aws_db_subnet_group.redash.name skip_final_snapshot = true availability_zones = [ "ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d", ] vpc_security_group_ids = [ aws_security_group.rds-redash.id ] lifecycle { ignore_changes = [ master_password, // passwordはsecrets managerで管理しています。 ] } } /* プライマリDB */ resource "aws_rds_cluster_instance" "redash" { identifier = "redash-1" cluster_identifier = aws_rds_cluster.redash.id instance_class = "db.t3.medium" engine = "aurora-postgresql" engine_version = "11.6" } ElastiCacheのTerraform RDSとほぼ同じです。 /* defaultはよくないので追加。必要があればパラメータを増やしていく */ resource "aws_elasticache_parameter_group" "redash" { name = "redash" family = "redis5.0" } resource "aws_elasticache_subnet_group" "redash" { name = "redash" subnet_ids = [ data.aws_subnet.private-subnet-1a.id, data.aws_subnet.private-subnet-1c.id, data.aws_subnet.private-subnet-1d.id, ] } /* Redashの6379ポートを開くセキュリティグループ */ resource "aws_security_group" "redis-redash" { name = "redis-redash" description = "for redash" vpc_id = data.aws_vpc.vpc.id } resource "aws_security_group_rule" "redis-redash-ingress" { type = "ingress" from_port = 6379 to_port = 6379 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.redis-redash.id } resource "aws_security_group_rule" "redis-redash-egress" { type = "egress" from_port = 0 to_port = 0 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.redis-redash.id } /* Redashでジョブのキューイングを行うRedis */ resource "aws_elasticache_cluster" "redash" { cluster_id = "redash" engine = "redis" node_type = "cache.t2.micro" num_cache_nodes = 1 parameter_group_name = aws_elasticache_parameter_group.redash.name subnet_group_name = aws_elasticache_parameter_group.redash.name security_group_ids = [ aws_security_group.redis-redash.id ] engine_version = "5.0.6" port = 6379 } ECS Fargate タイミーではFargate Serviceを構築するためのTerraform Moduleがあり、Redash構築でも利用しています。 CPUやメモリを 閾値 としたオートスケーリングや、firelensを利用したDatadog Logsへのログ配信が容易に行えるようになっています。 この説明については後日記事にしたいと思います。 FargateではRedashの 公式Dockerイメージ をコンテナで実行しています。 ECS Cluster内に4つのECS Serviceが動いており、それぞれの役割は以下です。 Server ... WebUIを提供するサービス Scheduled Worker ... スケジューリングされたクエリを処理する Adhoc Worker ... 都度実行されるクエリを処理する Scheduler ... Redisにjobをキューイングする Redashは実行するコマンドを変更することによって、それぞれの役割を振る舞うことができます。 さらに 環境変数 を設定することで柔軟に設定を変更することができます。 redash.io 環境変数 がどのように設定されているかを知ることで、それぞれのサービスの理解がしやすくなるかと思います。 ここではそれぞれのサービスで利用するタスク定義から、実行コマンドと設定した 環境変数 を抜粋して説明します。 Server ServerはALBに紐付けられWeb UIを提供します。 ユーザーが実行するクエリの処理はこのサービスでは行いません。 クエリはWorkerが処理し、 PostgreSQL に書き込まれた結果をWebUIが表示します。 " command ": [ " server " ] , " environment ": [ { " name ": " PYTHONUNBUFFERED ", " value ": " 0 " } , { " name ": " REDASH_ADDITIONAL_QUERY_RUNNERS ", " value ": " redash.query_runner.python " } , { " name ": " REDASH_HOST ", " value ": " https://example.com " } , { " name ": " REDASH_LOG_LEVEL ", " value ": " INFO " } , { " name ": " REDASH_MAIL_DEFAULT_SENDER ", " value ": " example@example.com " } , { " name ": " REDASH_MAIL_PORT ", " value ": " 587 " } , { " name ": " REDASH_MAIL_SERVER ", " value ": " smtp.sendgrid.net " } , { " name ": " REDASH_MAIL_USE_TLS ", " value ": " true " } , { " name ": " REDASH_MAIL_USERNAME ", " value ": " apikey " } , { " name ": " REDASH_REDIS_URL ", " value ": " redis://XXXXX.apne1.cache.amazonaws.com:6379 " } , { " name ": " REDASH_THROTTLE_LOGIN_PATTERN ", " value ": " 1000/minute " } , { " name ": " REDASH_WEB_WORKERS ", " value ": " 4 " } ] , " secrets ": [ { " valueFrom ": " /fargate/redash/REDASH_COOKIE_SECRET ", " name ": " REDASH_COOKIE_SECRET " } , { " valueFrom ": " /fargate/redash/REDASH_MAIL_PASSWORD ", " name ": " REDASH_MAIL_PASSWORD " } , { " valueFrom ": " /fargate/redash/REDASH_DATABASE_URL ", " name ": " REDASH_DATABASE_URL " } ] , REDASH_DATABASE_URL REDASH_COOKIE_SECRET REDASH_MAIL_PASSWORD は秘匿情報のためParameter Storeに保管し、値をコンテナ起動時に注入しています。 REDASH_DATABASE_URL が秘匿情報な理由はパスワードも含めた接続情報なためです。 postgres://<ユーザー名>:<パスワード>@ホスト名 といった文字列が格納されています。 注意すべき点は REDASH_THROTTLE_LOGIN_PATTERN です。これは /login エンドポイントへのレートリミットが設定されており、デフォルトで "50/hour" が設定されています。 FargateにおいてALBのヘルスチェックは有効にしておきたいところですが、Redashにはヘルスチェック用のパスが用意されておらず、ログインせずとも見られるページは /login だけでした。そのためレートリミットを緩和することでヘルスチェックができるようにしています。 メールの送信にはタイミーではSendGridを利用しています。 Adhoc Worker, Scheduled Worker Adhoc Workerはユーザーが都度実行するクエリを処理し、Scheduled Workerは定期実行されるクエリを処理します。 Redisのキューを受け取って処理を開始し、データソースに問い合わせた結果を PostgreSQL に保存します。 " command ": [ " worker " ] , " environment ": [ { " name ": " PYTHONUNBUFFERED ", " value ": " 0 " } , { " name ": " QUEUES ", " value ": " queries " } , { " name ": " REDASH_ADDITIONAL_QUERY_RUNNERS ", " value ": " redash.query_runner.python " } , { " name ": " REDASH_HOST ", " value ": " https://example.com " } , { " name ": " REDASH_LOG_LEVEL ", " value ": " INFO " } , { " name ": " REDASH_MAIL_DEFAULT_SENDER ", " value ": " example@example.com " } , { " name ": " REDASH_MAIL_PORT ", " value ": " 587 " } , { " name ": " REDASH_MAIL_SERVER ", " value ": " smtp.sendgrid.net " } , { " name ": " REDASH_MAIL_USE_TLS ", " value ": " true " } , { " name ": " REDASH_MAIL_USERNAME ", " value ": " apikey " } , { " name ": " REDASH_REDIS_URL ", " value ": " redis://XXXXX.apne1.cache.amazonaws.com:6379 " } , { " name ": " WORKERS_COUNT ", " value ": " 2 " } ] , " secrets ": [ { " valueFrom ": " /fargate/redash/REDASH_DATABASE_URL ", " name ": " REDASH_DATABASE_URL " } , { " valueFrom ": " /fargate/redash/REDASH_COOKIE_SECRET ", " name ": " REDASH_COOKIE_SECRET " } , { " valueFrom ": " /fargate/redash/REDASH_MAIL_PASSWORD ", " name ": " REDASH_MAIL_PASSWORD " } ] , 上記はAdhoc Workerのタスク定義ですが、 Scheduled Workerとの違いは QUEUES がqueriesかscheduled_queriesかどうかのみです。 カンマで区切って両方指定することで、1つのworkerで両方の責務を担うこともできます。 EC2の頃に インスタンス のCPUを押し上げていたのはこのAdhoc Workerでした。非エンジニアで SQL に慣れていないメンバーが多いため、パフォーマンスを考慮できず数分以上かかる重いクエリがたくさん叩かれることが原因でした。 そのためAdhoc WorkerのサービスのみCPUとメモリを増やし、コンテナの最低数/最大数を増やすことで解決しました。 サービスを分割したことで、特定のコンテナのみスペックを増強することができるようになったのもFargate化の利点です。 Scheduler Redash Schedulerは Python のライブラリ RQ Scheduler を利用し、Redisにキューを追加します。 " command ": [ " scheduler " ] , " environment ": [ { " name ": " PYTHONUNBUFFERED ", " value ": " 0 " } , { " name ": " QUEUES ", " value ": " celery " } , { " name ": " REDASH_ADDITIONAL_QUERY_RUNNERS ", " value ": " redash.query_runner.python " } , { " name ": " REDASH_HOST ", " value ": " https://example.com " } , { " name ": " REDASH_LOG_LEVEL ", " value ": " INFO " } , { " name ": " REDASH_MAIL_DEFAULT_SENDER ", " value ": " example@example.com " } , { " name ": " REDASH_MAIL_PORT ", " value ": " 587 " } , { " name ": " REDASH_MAIL_SERVER ", " value ": " smtp.sendgrid.net " } , { " name ": " REDASH_MAIL_USE_TLS ", " value ": " true " } , { " name ": " REDASH_MAIL_USERNAME ", " value ": " apikey " } , { " name ": " REDASH_REDIS_URL ", " value ": " redis://XXXXX.apne1.cache.amazonaws.com:6379 " } , { " name ": " WORKERS_COUNT ", " value ": " 5 " } ] , " secrets ": [ { " valueFrom ": " /fargate/redash/REDASH_DATABASE_URL ", " name ": " REDASH_DATABASE_URL " } , { " valueFrom ": " /fargate/redash/REDASH_COOKIE_SECRET ", " name ": " REDASH_COOKIE_SECRET " } , { " valueFrom ": " /fargate/redash/REDASH_MAIL_PASSWORD ", " name ": " REDASH_MAIL_PASSWORD " } ] , QUEUES を celery に指定することで、Schedulerとして振る舞います。 サービスを分割して運用しているものの負荷が全然かからないため、Scheduled Workerとの統合も検討しています。 firelensを利用した、Datadog Logsへのログ転送 上記で紹介した4つのECS Serviceのログは、firelensを通してDatadog LogsとS3に転送されています。 id:sion_cojp のこちらの記事で詳しく紹介しています。 sioncojp.hateblo.jp Datadog Dashboard による監視 ALB, ECS Service, RDSを ダッシュ ボードで一覧できるようにしました。 DatadogもTerraformで管理しており、 ダッシュ ボード作成作業はコピペで済むようになって楽です。Datadogはapplyが早いのもよいです。 ダッシュ ボードのTerraform resource "datadog_dashboard" "redash" { title = "[${local.env}] ${var.service_name}" description = "Created using the Datadog provider in Terraform" layout_type = "ordered" widget { group_definition { layout_type = "ordered" title = "ALB: ${var.service_name}" widget { timeseries_definition { title = "リクエスト数" request { q = "sum:aws.applicationelb.request_count{name:${var.service_name},env:${local.env},account:${local.name}} by {availability-zone}.as_count()" display_type = "area" } } } widget { timeseries_definition { title = "4xxリクエスト数" request { q = "sum:aws.applicationelb.httpcode_elb_4xx{name:${var.service_name},env:${local.env},account:${local.name}} by {availability-zone}.as_count()" display_type = "area" } } } widget { timeseries_definition { title = "5xxリクエスト数" request { q = "sum:aws.applicationelb.httpcode_elb_5xx{name:${var.service_name},env:${local.env},account:${local.name}} by {availability-zone}.as_count()" display_type = "area" } } } widget { timeseries_definition { title = "コネクション数" request { q = "sum:aws.applicationelb.active_connection_count{name:${var.service_name},env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "ALBに紐づいてる正常なコンテナ数" request { q = "sum:aws.applicationelb.healthy_host_count{name:${var.service_name},env:${local.env},account:${local.name}} by {availability-zone}.as_count()" display_type = "area" } } } widget { timeseries_definition { title = "ALBに紐づいてる異常なコンテナ数" request { q = "sum:aws.applicationelb.un_healthy_host_count{name:${var.service_name},env:${local.env},account:${local.name}} by {availability-zone}.as_count()" display_type = "area" } } } } } widget { group_definition { layout_type = "ordered" title = "ECS: redash-server" widget { timeseries_definition { title = "起動タスク数" request { q = "sum:aws.ecs.service.running{servicename:redash-server,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "sum:aws.ecs.service.pending{servicename:redash-server,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "CPU Utilization(MAX)" request { q = "max:aws.ecs.service.cpuutilization.maximum{servicename:redash-server,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "max:aws.ecs.service.cpuutilization{servicename:redash-server,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "Memory Utilization(MAX)" request { q = "max:aws.ecs.service.memory_utilization.maximum{servicename:redash-server,env:${local.env},account:${local.name}}" display_type = "area" } } } } } widget { group_definition { layout_type = "ordered" title = "ECS: redash-scheduler" widget { timeseries_definition { title = "起動タスク数" request { q = "sum:aws.ecs.service.running{servicename:redash-scheduler,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "sum:aws.ecs.service.pending{servicename:redash-scheduler,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "CPU Utilization(MAX)" request { q = "max:aws.ecs.service.cpuutilization.maximum{servicename:redash-scheduler,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "max:aws.ecs.service.cpuutilization{servicename:redash-scheduler,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "Memory Utilization(MAX)" request { q = "max:aws.ecs.service.memory_utilization.maximum{servicename:redash-scheduler,env:${local.env},account:${local.name}}" display_type = "area" } } } } } widget { group_definition { layout_type = "ordered" title = "ECS: redash-scheduled-worker" widget { timeseries_definition { title = "起動タスク数" request { q = "sum:aws.ecs.service.running{servicename:redash-scheduled-worker,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "sum:aws.ecs.service.pending{servicename:redash-scheduled-worker,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "CPU Utilization(MAX)" request { q = "max:aws.ecs.service.cpuutilization.maximum{servicename:redash-scheduled-worker,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "max:aws.ecs.service.cpuutilization{servicename:redash-scheduled-worker,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "Memory Utilization(MAX)" request { q = "max:aws.ecs.service.memory_utilization.maximum{servicename:redash-scheduled-worker,env:${local.env},account:${local.name}}" display_type = "area" } } } } } widget { group_definition { layout_type = "ordered" title = "ECS: redash-adhoc-worker" widget { timeseries_definition { title = "起動タスク数" request { q = "sum:aws.ecs.service.running{servicename:redash-adhoc-worker,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "sum:aws.ecs.service.pending{servicename:redash-adhoc-worker,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "CPU Utilization(MAX)" request { q = "max:aws.ecs.service.cpuutilization.maximum{servicename:redash-adhoc-worker,env:${local.env},account:${local.name}}" display_type = "area" } request { q = "max:aws.ecs.service.cpuutilization{servicename:redash-adhoc-worker,env:${local.env},account:${local.name}}" display_type = "area" } } } widget { timeseries_definition { title = "Memory Utilization(MAX)" request { q = "max:aws.ecs.service.memory_utilization.maximum{servicename:redash-adhoc-worker,env:${local.env},account:${local.name}}" display_type = "area" } } } } } widget { group_definition { layout_type = "ordered" title = "RDS: ${var.service_name}" widget { timeseries_definition { title = "CPU Utilization(MAX)" request { q = "max:aws.rds.cpuutilization{account:${local.name},dbclusteridentifier:${var.service_name}}" display_type = "area" } } } widget { timeseries_definition { title = "DBコネクション数(MAX)" request { q = "max:aws.rds.database_connections{account:${local.name},dbclusteridentifier:${var.service_name}}" display_type = "area" } } } widget { timeseries_definition { title = "空きストレージ容量 (MB)" request { q = "max:aws.rds.free_storage_space{account:${local.name},dbclusteridentifier:${var.service_name}}" display_type = "area" } } } widget { timeseries_definition { title = "使用可能なメモリ(MB)" request { q = "max:aws.rds.freeable_memory{account:${local.name},dbclusteridentifier:${var.service_name}}" display_type = "area" } } } } } } その他 Redashを AWS で構築するにあたって、Route53や ACM , WAFなどを使用しましたが、今回は記事が長くなってしまうため割愛します。 また、 stg 環境とprod環境を AWS アカウント単位分けており、 stg 環境として全く同じ構成のRedashを立てています。 理由はredashのバージョンアップや saml を使ったSSO認証の検証のためです。 今はコンテナ数を0にして寝かせています。 Fargateに移行した利点 抱えていた課題がほぼ解消できた 1. オートスケールできないため、クエリが詰まってCPUが100%になってサービスが停止する → オートスケールができるようになり、サービスが停止することはなくなりました。 2. セットアップしたエンジニアが退社しており、インフラ構成図やノウハウ、IaCによる管理ができていない。 → 全てコードで管理されている。構成図や wiki も残すことで、後任者がキャッチアップできるようになりました。 3. ダッシュ ボードなどのデータの定期的なバックアップができていない。 → AWS マネージドサービスに移行し、RDSのスナップショットで解決しました。 4. v7系からv8系へのアップデートがしたいが、アップデートによる影響範囲がわからず恐怖感がある。 → stg 環境があるので、本番環境で実施する前に試すことができるようになりました。 5. 事業に大きく関わるサービスなのにも関わらず、モニタリングやアラートができていない。 → datadogでモニタリングができるようになりました。まだコンテナ数などの調整中のため、アラートは保留としています。 また移行前は週に数回オンコールが発生していたが、移行してからほぼ0になりました。 サービスをきちんと分離したことで、負荷がかかることが多いAdhoc Workerのみスペックを上げる事が可能になった それまでは重いクエリを実行するとEC2 インスタンス のCPUが100%に達して他のユーザーにも影響を与えてしまっていたのが、サービスを分離したことでAdhoc Worker以外のサービスへの影響を減らすことができるようになりました。かつAdhoc Workerのみスペックを上げることができるようになりました。コンテナとサーバレスの特性をうまく活かすことができたと思っています。 まだ残っている課題 Redashのバージョンをv7からv8に上げる v8にアップデートできるとクエリ名を日本語で正しく検索できるようになるため、社内からアップデートしてほしいと要望があります。しかし今回のFargate移行でアップデートしやすくなったため、近いうちに着手します。 ログイン認証をSSOで行う タイミーでは従業員に発行する各種アカウントをGSuiteでのSSOでできるよう移行を進めています。Redashも SAML 認証によるSSOに対応しているので、次にやっていきたいと思っています。 まとめ 今回EC2で動いていたRedashをFargateに移行することによって、Redashにまつわる事柄全てをマネージドサービスとIaCで管理することができるようになりました。タイミーのSREがアプリケーションをどのように運用しているかも紹介できたかと思っています。 また今回よりタイミーのプロダクトチームのブログを開設することになりました。SRE/サーバーサイドエンジニア/フロントエンドエンジニア/デザイナーそれぞれの、タイミーのプロダクトにまつわる記事を投稿していきたいと思っております。ぜひお楽しみに!