😀

RedashをECS上で構築して, Cloudformationでコード管理する

2021/05/28に公開

1. はじめに

Redashの環境構築については、公式のQuickNaviに様々な方法が紹介されており、Docker imageや、AWSの向けにはAMIも提供されてます。
そのためシンプルにEC2上で運用するのであれば、比較的容易に構築が可能です。

ただしQuickNaviの構成は、 Dockerプロセス各種が単一のEC2インスタンス内で実行されます。
そのためサクッと検証するには良いのですが、 本番運用において可用性の点で心許ないです。
そこで今回は、 ServerやPostgreSQL、 Redis等をECS上に冗長化構成で構築、
最終的にはその構成をCloudforamtionを使用してコード化を行います。

QuickNavi構成
redash-on-ecs-EC2.png

2. そもそもRedashとは?

既に他のサイトで詳しく紹介されていますので、ここでは簡単に紹介します。

RedashとはSQL/NoSQL/BigData/APIなどの様々なデータソースに対しての接続をサポートしており、各種データの統合、分析を行うことができるOSSツールです。
そして機能については以下を提供しています。

  • クエリティエディタ
  • ダッシュボード
  • アラート
  • API

つまり、簡単に言えばデータベースに対してクエリーを投げたり、BigQuery、Athenaなどのクエリ系サービスからデータを抽出して、
その結果をAPIとして提供したり、ダッシュボードで可視化できますよーというものです。

クエリティエディタ

様々なデータソースに対してのクエリ結果を使用し、異なるデータソースを統合することが可能です。
(図はサンプルです)

redash-query-sample.png

ダッシュボード

クエリエディタで定義したクエリーの結果をダッシュボードで可視化できます。
またクエリエディタで定義したクエリーの実行はスケジューラ設定が可能なので、データの自動更新も可能です。

redash-dashboard.png

API

図はないですが、クエリ結果のAPI化も可能です。
これが結構便利で、クエリで抽出した結果を他のシステムに提供したい場合に、 データ転送用のjobを仕込んだり、APIを構築したりの工数が取れないケースで、Redashを使えば簡単にデータへの接続エンドポイントを提供することが可能になります。

2.2 データソース一覧

利用可能なデータソース一覧については一部ですが以下に記載しています.
以下のようにSQL/NoSQL/BigDataなど様々なのデータストアへ接続をサポートしています。
また全てを確認したい方は下記公式のインテグレーションリンクから確認可能です。

  • Amazon Athena
  • Amazon Aurora
  • Amazon DynamoDB
  • Amazon Redshift
  • Cassandra
  • Elasticsearch
  • Google BigQuery
  • Hive
  • Impala
  • Microsoft SQL Server
  • MongoDB
  • PostgreSQL
  • Snowflake
  • TreasureData

公式integration

https://redash.io/integrations/

3. ECS上で構築

QuickNavi構成ではEC2の単一インスタンス上に、ServerやWorker、PostgreSQL、RedisサーバがそれぞれDockerプロセスとして立ち上がっています。
これではそれぞれのプロセスが一つ停止しただけでサービスとして機能しなくなってしまい、言うまでもなくEC2がダウンした際にもサービスは停止してしまいます。

そこで今回は各DockerプロセスをECS上に展開し冗長化構成にしてみましょう。
構成について以下の図に表します。

redash-on-ecs-ECS (1).png

構築の流れとしては、まずそれぞれが単一障害点とならないように、
データストアとして使用しているPostgreSQLをAWSのRDSへ移行し、MultiAZ構成に
そしてキャッシュサーバとして使用しているRedisについては、ElaticCacheへ移行し、こちらもMultiAZ構成かつ自動フェイルオーバをenabledにします。

そしてRedashは以下の各プロセスで構成されており、これらをECS上のTaskとしてSerivice内で起動、管理します。

  • Server: 可視化ダッシュボードやクエリを設定するWEBアプリケーション
  • Worker: 設定したクエリを実行するWorker
  • Scheduler: 設定したクエリの実行Scheduler

3.1 構築手順

本章では実際の構築手順をまとめていきます。
また構築手順の解説では、Cloudformationの定義例もまとめていきますが、RDSとElasticCacheはコード化対象から除外しています。
これはCloudforamtionによるdeploy処理の際に、意図しないreplace処理が走ることを避けるためです。
この辺りの管理方法は運用フローに依存するため、必要であれば追加してもらえればと思います。

RDS,ElasticCache

こちらはまず空のインスタンスをそれぞれ作成します。
ElasticCacheの「マルチAZ」「自動フェイルオーバー」はenabledにしておきましょう。
また、後述のcreate_dbタスクによって初期スキーマの定義を行うため、この時点では空の状態で問題ありません。

Task用Role

こちらはECS Task実行用Roleです。
ポリシーはAWS管理のAmazonECSTaskExecutionRolePolicyCloudWatchAgentServerPolicyを使用していますが、必要の応じてカスタマイズしてください。

  #  -------------------- Task ExecutionRole  --------------------
  TascExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub ${ProjectName}-task-execution-role
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
        - !Sub arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole

ALB

ECSTaskと紐付けるALBの定義です。
ALBのSchemeは公開するためinternet-facingにしていますが、非公開の場合はinternalにしてください。
ALBTargetGroupでは、予め作成しておいたVPCのIDを定義、
ALBListenerでは、予めACMで作成しておいたCertificateのARNを定義します。

  # -------------------- ALB --------------------
  ALB: #
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${ProjectName}-alb
      Scheme: internet-facing # 公開したくない場合はinternalで
      SecurityGroups:
        - !FindInMap [ !Ref Env, Redash, ALBSecurityGroupId ]
      Subnets: !FindInMap [ !Ref Env, Redash, SubnetIds ]

  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${ProjectName}-tg
      VpcId: !FindInMap [ Common, Redash, VpcId ]
      Protocol: HTTP
      Port: 5000
      TargetType: ip
      HealthCheckPath: /ping
  # ACMは手動作成したものを使用しています.
  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn:
      - ALBTargetGroup
      - ALB
    Properties:
      LoadBalancerArn: !Ref ALB
      Protocol: HTTPS
      Port: 443
      Certificates:
        - CertificateArn: !FindInMap [ Common, Redash, ACMArn ]
      SslPolicy: ELBSecurityPolicy-2016-08
      DefaultActions:
        - TargetGroupArn: !Ref ALBTargetGroup
          Type: forward

Route53

先ほど作成したALBと、予め作成しておいたホストゾーンを紐付けます。
!GetAtt ALB.CanonicalHostedZoneID!GetAtt ALB.DNSNameでは作成したALBのHostedZoneIdやDNSNameを取得しています。

※ALB ReturnValues


  RedashRoute53:
    Type: AWS::Route53::RecordSet
    DependsOn:
      - ALBListener
    Properties:
      Type: A
      AliasTarget:
        HostedZoneId: !GetAtt ALB.CanonicalHostedZoneID
        EvaluateTargetHealth: true
        DNSName: !GetAtt ALB.DNSName
      HostedZoneId: !FindInMap [ Common, Redash, HostZoneId ] #Replacement
      Name: !Sub #Replacement
        - ${SubDomain}.${HostZoneDomain}
        - SubDomain: !FindInMap [ !Ref Env, Redash, SubDomain ]
          HostZoneDomain: !FindInMap [ Common, Redash, HostZoneDomain ]

Task

以下ECS Task定義です。
今回はECSのFargeteを使用するため RequiresCompatibilitiesFARGATEに。
ContainerDefinitionsはRedashのプロセスである, server, scheduler, workerについて定義します(詳細は後述)

  # -------------------- All Task定義 --------------------
  ALLTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      NetworkMode: awsvpc
      Cpu: 4096
      Memory: 16384
      Family: !Sub ${ProjectName}-all-task
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt TascExecutionRole.Arn
      ContainerDefinitions:
        # 省略.詳細は後述
        # server 
        # scheduler 
        # worker 

Task ContainerDefinitions: Server

以下はTask上で実行するContainerのServer用定義です。

指定Imageはredash/redash:8.0.0のように公式で提供されているImageのversionを指定してください。
Docker Hub redash images

またserver用コンテナのため、Commandはserverを、またEnvironment設定のREDASH_WEB_WORKERSは必ず1をセットしてください。

      ContainerDefinitions:
        # server
        - Image: !FindInMap [ Common, Redash, DockerImage ]
          Name: !Sub ${ProjectName}-server-container
          EntryPoint:
            - /app/bin/docker-entrypoint
          WorkingDirectory: /app
          Command:
            - 'server'
          Environment:
            - Name: 'PYTHONUNBUFFERED'
              Value: '0'
            - Name: 'REDASH_ALLOW_SCRIPTS_IN_USER_INPUT'
              Value: 'true'
            - Name: 'REDASH_COOKIE_SECRET'
              Value: !FindInMap [ !Ref Env, Redash, CookieSecret ]
            - Name: 'REDASH_SECRET_KEY'
              Value: !FindInMap [ !Ref Env, Redash, SecretKey ]
            - Name: 'REDASH_DATABASE_URL'
              Value: !Sub
                - postgresql://${RDSUser}:${RDSPass}@${RDSEndpoint}/postgres
                - RDSUser: !FindInMap [ !Ref Env, RDS, User ]
                  RDSPass: !FindInMap [ !Ref Env, RDS, Pass ]
                  RDSEndpoint: !FindInMap [ !Ref Env, RDS, Endpoint ]
            - Name: 'REDASH_REDIS_URL'
              Value: !Sub
                - redis://${ElasticCacheEndpoint}:${ElasticCachePort}/0
                - ElasticCacheEndpoint: !FindInMap [ !Ref Env, Redis, Endpoint ]
                  ElasticCachePort: !FindInMap [ !Ref Env, Redis, Port ]
            - Name: 'REDASH_HOST'
              Value: ''
            - Name: 'REDASH_DATE_FORMAT'
              Value: 'YY/MM/DD'
            - Name: 'REDASH_LOG_LEVEL'
              Value: !If [ IsPrd, 'INFO', 'DEBUG' ]
            - Name: 'REDASH_WEB_WORKERS'
              Value: '1'
            - Name: 'REDASH_CSV_WRITER_ENCODING'
              Value: 'cp932'
          PortMappings:
            - ContainerPort: 5000
              HostPort: 5000
              Protocol: 'tcp'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-create-group: true
              awslogs-group: !Sub /ecs/${ProjectName}
              awslogs-region: !Sub ${AWS::Region}
              awslogs-stream-prefix: 'server'

その他environmentの解説は以下です。

項目名 内容
REDASH_COOKIE_SECRET DBの暗号化で使用
REDASH_SECRET_KEY DBの暗号化で使用
REDASH_DATABASE_URL 先ほど作成したPostgreSQLに関する接続設定
REDASH_REDIS_URL 先ほど作成したRedisに関する接続設定
REDASH_CSV_WRITER_ENCODING CSVエクスポート時のエンコーディング設定です。

Task ContainerDefinitions: Worker

以下はTask上で実行するContainerのWorker用定義です。

        # worker
        - Image: !FindInMap [ Common, Redash, DockerImage ]
          Name: !Sub ${ProjectName}-worker-container
          EntryPoint:
            - /app/bin/docker-entrypoint
          WorkingDirectory: /app
          Command:
            - 'worker'
          Environment:
            - Name: 'PYTHONUNBUFFERED'
              Value: '0'
            - Name: 'REDASH_ALLOW_SCRIPTS_IN_USER_INPUT'
              Value: 'true'
            - Name: 'REDASH_COOKIE_SECRET'
              Value: !FindInMap [ !Ref Env, Redash, CookieSecret ]
            - Name: 'REDASH_SECRET_KEY'
              Value: !FindInMap [ !Ref Env, Redash, SecretKey ]
            - Name: 'QUEUES'
              Value: 'queries,scheduled_queries,schemas'
            - Name: 'REDASH_DATABASE_URL'
              Value: !Sub
                - postgresql://${RDSUser}:${RDSPass}@${RDSEndpoint}/postgres
                - RDSUser: !FindInMap [ !Ref Env, RDS, User ]
                  RDSPass: !FindInMap [ !Ref Env, RDS, Pass ]
                  RDSEndpoint: !FindInMap [ !Ref Env, RDS, Endpoint ]
            - Name: 'REDASH_REDIS_URL'
              Value: !Sub
                - redis://${ElasticCacheEndpoint}:${ElasticCachePort}/0
                - ElasticCacheEndpoint: !FindInMap [ !Ref Env, Redis, Endpoint ]
                  ElasticCachePort: !FindInMap [ !Ref Env, Redis, Port ]
            - Name: 'REDASH_HOST'
              Value: ''
            - Name: 'REDASH_DATE_FORMAT'
              Value: 'YY/MM/DD'
            - Name: 'REDASH_LOG_LEVEL'
              Value: !If [ IsPrd, 'INFO', 'DEBUG' ]
            - Name: 'WORKERS_COUNT'
              Value: '3'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-create-group: true
              awslogs-group: !Sub /ecs/${ProjectName}
              awslogs-region: !Sub ${AWS::Region}
              awslogs-stream-prefix: 'worker'

Task ContainerDefinitions: Scheduler

以下はTask上で実行するContainerのScheduler用定義です。

        # scheduler
        - Image: !FindInMap [ Common, Redash, DockerImage ]
          Name: !Sub ${ProjectName}-scheduler-container
          EntryPoint:
            - /app/bin/docker-entrypoint
          WorkingDirectory: /app
          Command:
            - 'scheduler'
          Environment:
            - Name: 'PYTHONUNBUFFERED'
              Value: '0'
            - Name: 'REDASH_ALLOW_SCRIPTS_IN_USER_INPUT'
              Value: 'true'
            - Name: 'REDASH_COOKIE_SECRET'
              Value: !FindInMap [ !Ref Env, Redash, CookieSecret ]
            - Name: 'REDASH_SECRET_KEY'
              Value: !FindInMap [ !Ref Env, Redash, SecretKey ]
            - Name: 'QUEUES'
              Value: 'celery'
            - Name: 'REDASH_DATABASE_URL'
              Value: !Sub
                - postgresql://${RDSUser}:${RDSPass}@${RDSEndpoint}/postgres
                - RDSUser: !FindInMap [ !Ref Env, RDS, User ]
                  RDSPass: !FindInMap [ !Ref Env, RDS, Pass ]
                  RDSEndpoint: !FindInMap [ !Ref Env, RDS, Endpoint ]
            - Name: 'REDASH_REDIS_URL'
              Value: !Sub
                - redis://${ElasticCacheEndpoint}:${ElasticCachePort}/0
                - ElasticCacheEndpoint: !FindInMap [ !Ref Env, Redis, Endpoint ]
                  ElasticCachePort: !FindInMap [ !Ref Env, Redis, Port ]
            - Name: 'REDASH_HOST'
              Value: ''
            - Name: 'REDASH_DATE_FORMAT'
              Value: 'YY/MM/DD'
            - Name: 'REDASH_LOG_LEVEL'
              Value: !If [ IsPrd, 'INFO', 'DEBUG' ]
            - Name: 'WORKERS_COUNT'
              Value: '1'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-create-group: true
              awslogs-group: !Sub /ecs/${ProjectName}
              awslogs-region: !Sub ${AWS::Region}
              awslogs-stream-prefix: 'scheduler'

ECS Service

  # ------------------------ Service ------------------------
  ECSService:
    Type: AWS::ECS::Service
    DependsOn:
      - ALLTaskDefinition
      - ALBListener
    Properties:
      Cluster: !Sub ${ProjectName}
      ServiceName: !Sub ${ProjectName}-all-task-service
      DesiredCount: 1
      LaunchType: FARGATE
      LoadBalancers:
        - ContainerName: !Sub ${ProjectName}-server-container
          ContainerPort: 5000
          TargetGroupArn: !Ref ALBTargetGroup
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !FindInMap [ !Ref Env, Redash, AllTaskSecurityGroupId ]
          Subnets: !FindInMap [ !Ref Env, Redash, SubnetIds ]
          AssignPublicIp: ENABLED
      TaskDefinition: !Ref ALLTaskDefinition
      PropagateTags: TASK_DEFINITION
      EnableECSManagedTags: true

3.2 create_dbタスクによるInitialize

3.1で全体の環境を構築したら、最後にPostgreSQL,Redisの初期設定を行います。
初期設定に関する処理についても、ECSのタスクを使用して実行します。
一度初期設定を行えば、今後使用することはないのでCfn化する必要はありませんが、
以下にタスクに関する定義を載せておきます。

Commandはcreate_dbを指定してください。

  CreateDBTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      Family: !Sub ${ProjectName}-create_db
      RequiresCompatibilities:
        - 'FARGATE'
      NetworkMode: 'awsvpc'
      ExecutionRoleArn: !Sub ${ExecuteRoleArn}
      Cpu: 4096
      Memory: 16384
      ContainerDefinitions:
        - Image: !FindInMap [ Common, Redash, DockerImage ]
          Name: !Sub ${ProjectName}-create_db-container
          Command:
            - 'create_db'
          Environment:
            - Name: 'PYTHONUNBUFFERED'
              Value: '0'
            - Name: 'REDASH_ALLOW_SCRIPTS_IN_USER_INPUT'
              Value: 'true'
            - Name: 'REDASH_DATABASE_URL'
              Value: !Sub
                - postgresql://${User}:${Pass}@${Endpoint}/${DB}
                - User: !FindInMap [ !Ref Env, RDS, User ]
                  Pass: !FindInMap [ !Ref Env, RDS, Pass ]
                  Endpoint: !FindInMap [ !Ref Env, RDS, Endpoint ]
                  DB: !FindInMap [ !Ref Env, RDS, DB ]
            - Name: 'REDASH_REDIS_URL'
              Value: !Sub
                - redis://${Endpoint}:${Port}/0
                - Endpoint: !FindInMap [ !Ref Env, Redis, Endpoint ]
                  Port: !FindInMap [ !Ref Env, Redis, Port ]
            - Name: 'REDASH_HOST'
              Value: ''
            - Name: 'REDASH_DATE_FORMAT'
              Value: 'YY/MM/DD'
            - Name: 'REDASH_LOG_LEVEL'
              Value: 'INFO'
            - Name: 'REDASH_PASSWORD_LOGIN_ENABLED'
              Value: false

ここまでで、構築は完了です。

最後に

いかがだったでしょうか?
RedashはDockerで提供されているため、ECS上での構築も比較的容易だったのではないでしょうか?

Redashを使用すれば、システムの開発やコーディング不要で、データの抽出や提供が可能になります。
ぜひ使ってみてください。

また、以下に今回使用したCloudformationに関する定義ファイル載せておきます。
https://github.com/kohei789/redash-on-ecs-example

Discussion