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

# 1. はじめに

Redashの環境構築については、[公式のQuickNavi](https://redash.io/help/open-source/setup#Create-an-Instance)に様々な方法が紹介されており、Docker imageや、AWSの向けにはAMIも提供されてます。
そのためシンプルにEC2上で運用するのであれば、比較的容易に構築が可能です。

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


*QuickNavi構成*
![redash-on-ecs-EC2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63356/b704a046-5104-f2fb-45b4-fe930e124b55.png)

# 2. そもそもRedashとは?

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

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

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

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

## クエリティエディタ

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

![redash-query-sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63356/1f7d0955-6787-0300-10c2-fb213d283323.png)

## ダッシュボード

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

![redash-dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63356/f1b025c9-b824-0843-35fb-45f6a49afe78.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構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63356/b704a046-5104-f2fb-45b4-fe930e124b55.png)ではEC2の単一インスタンス上に、ServerやWorker、PostgreSQL、RedisサーバがそれぞれDockerプロセスとして立ち上がっています。
これではそれぞれのプロセスが一つ停止しただけでサービスとして機能しなくなってしまい、言うまでもなくEC2がダウンした際にもサービスは停止してしまいます。

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

![redash-on-ecs-ECS (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63356/229ac238-804f-fcbe-2546-398b76436502.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管理の`AmazonECSTaskExecutionRolePolicy`と`CloudWatchAgentServerPolicy`を使用していますが、必要の応じてカスタマイズしてください。

```yml
  #  -------------------- 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を定義します。


```yml
  # -------------------- 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](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html#aws-resource-elasticloadbalancingv2-loadbalancer-return-values)

```yml

  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を使用するため `RequiresCompatibilities`FARGATEに。
ContainerDefinitionsはRedashのプロセスである, server, scheduler, workerについて定義します(詳細は後述)

```yml
  # -------------------- 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](https://hub.docker.com/r/redash/redash/tags?page=1&ordering=last_updated)

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

```yml
      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用定義です。


```yml
        # 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用定義です。

```yml
        # 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



```yml
  # ------------------------ 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`を指定してください。


```yml
  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