📊

Redash を AWS Copilot CLI で構築する

2023/08/29に公開
2

はじめに

こんにちは。レンティオの小島です。

レンティオでは BI ツールとして Redash を利用しています。
Redash は OSS であり、マネージドサービスがないため自前でホスティングする必要があります。

もともと AWS に構築していましたが、数年経ち当時構築したメンバーがいないこともあってメンテナンスに課題がありました。そこで今回社内でも馴染みのある[1] AWS Copilot CLI を使ってリプレイスしたため、その紹介をしたいと思います。

ちなみに Redash by AWS Copilot CLI には先駆者がおり、たいへん参考にさせていただきました。
https://github.com/kataring/aws-copilot-redash

AWS Copilot CLI とは

Amazon ECS (Fargate) で稼働するアプリケーションを簡単に構築できるツールです。
独自の YAML 形式の設定ファイルと CLI 操作で基本的には完結します。

Application, Environment, Service という階層構造の概念があり、順に作成・デプロイしていくとリソースができあがります。

  • Application (name: redash)
    • Environment (name: production)
      VPC, ALB, ECS クラスター など
      • Service (name: server, scheduler, ...)
        ECS サービス, ロググループ など

Application

以下で作成します。作業ディレクトリ内に copilot/ というディレクトリが作られます。

copilot app init redash

Environment

以下で作成します。copilot/ 内に Manifest ファイルが作られます。

copilot env init --name production

Manifest ファイルをドキュメントに倣って適宜編集します。
今回は以下の要件があるとします。

  • Route 53 の example.com というホストゾーンを使いたい
  • 社内の IP アドレスからのみアクセスできるようにしたい
Manifest ファイル
copilot/environments/production/manifest.yml
name: production
type: Environment

network:
  vpc:
    cidr: 'xxx.xxx.xxx.xxx/16'
    subnets:
      public:
        - cidr: 'xxx.xxx.xxx.xxx/24'
        - cidr: 'xxx.xxx.xxx.xxx/24'
      private:
        - cidr: 'xxx.xxx.xxx.xxx/24'
        - cidr: 'xxx.xxx.xxx.xxx/24'

http:
  public:
    certificates:
      - arn:aws:acm:ap-northeast-1:xxxxxxxx # example.com の証明書 ARN
    ingress:
      source_ips:
        - 'xxx.xxx.xxx.xxx/32' # 社内 IP 1
        - 'xxx.xxx.xxx.xxx/32' # 社内 IP 2

編集できたら以下でデプロイします。CloudFormation で VPC 等のリソースが作成されるのを確認できます。

copilot env deploy --name production

Service

Service にはいくつか種類があり、今回は以下の2種類を利用します。

  • Load Balanced Web Service
    • ALB 経由でリクエストを受けられるサービス
  • Backend Service
    • インターネットに露出しないサービス

Redash の最新バージョンである v10 を動かすには以下のサービスが必要となります。
(公式ドキュメントには明記されてない気がするのですが、リリースノートが参考になります)

name type command QUEUES 説明
server Load Balanced Web Service server ブラウザからのリクエストを受ける Web サーバー
scheduler Backend Service scheduler 定期実行クエリをキューに入れる
scheduled-worker Backend Service worker scheduled_queries,schemas 定期実行クエリのキューを処理する
adhoc-worker Backend Service worker queries ブラウザ実行クエリのキューを処理する
worker Backend Service worker periodic emails default そのほかのキューを処理する

それぞれ以下で作成します。対話で種類を聞かれます。

copilot svc init --name xxxx

Manifest ファイルを編集します。

server の Manifest ファイル
copilot/server/manifest.yml
name: server
type: Load Balanced Web Service

http:
  path: '/'
  healthcheck: '/ping'
  alias: redash.example.com # ALB を指す A レコードが作られます
  hosted_zone: xxxxxxxx     # example.com のホストゾーン ID

image:
  location: redash/redash:10.1.0.b50633
  port: 5000

cpu: 512
memory: 2048
count: 1
exec: true
command: 'server'

variables:
  REDASH_WEB_WORKERS: 4

# 後で説明します
secrets:
  REDASH_DATABASE_URL: /copilot/redash/production/secrets/redash_database_url
  REDASH_REDIS_URL: /copilot/redash/production/secrets/redash_redis_url
scheduler の Manifest ファイル
copilot/scheduler/manifest.yml
name: scheduler
type: Backend Service

image:
  location: redash/redash:10.1.0.b50633

cpu: 256
memory: 512
count: 1
exec: true
command: 'scheduler'

# 後で説明します
secrets:
  REDASH_DATABASE_URL: /copilot/redash/production/secrets/redash_database_url
  REDASH_REDIS_URL: /copilot/redash/production/secrets/redash_redis_url
scheduled-worker の Manifest ファイル
copilot/scheduled-worker/manifest.yml
name: scheduled-worker
type: Backend Service

image:
  location: redash/redash:10.1.0.b50633

cpu: 512
memory: 2048
count: 1
exec: true
command: 'worker'

variables:
  QUEUES: 'scheduled_queries,schemas'
  WORKERS_COUNT: 2

# 後で説明します
secrets:
  REDASH_DATABASE_URL: /copilot/redash/production/secrets/redash_database_url
  REDASH_REDIS_URL: /copilot/redash/production/secrets/redash_redis_url
adhoc-worker の Manifest ファイル
copilot/adhoc-worker/manifest.yml
name: adhoc-worker
type: Backend Service

image:
  location: redash/redash:10.1.0.b50633

cpu: 512
memory: 2048
count: 1
exec: true
command: 'worker'

variables:
  QUEUES: 'queries'
  WORKERS_COUNT: 4

# 後で説明します
secrets:
  REDASH_DATABASE_URL: /copilot/redash/production/secrets/redash_database_url
  REDASH_REDIS_URL: /copilot/redash/production/secrets/redash_redis_url
worker の Manifest ファイル
copilot/adhoc-worker/manifest.yml
name: worker
type: Backend Service

image:
  location: redash/redash:10.1.0.b50633

cpu: 256
memory: 512
count: 1
exec: true
command: 'worker'

variables:
  QUEUES: 'periodic emails default'
  WORKERS_COUNT: 1

# 後で説明します
secrets:
  REDASH_DATABASE_URL: /copilot/redash/production/secrets/redash_database_url
  REDASH_REDIS_URL: /copilot/redash/production/secrets/redash_redis_url

Secret

Redash の稼働には PostgreSQL と Redis が必要です。
Service の Manifest ファイルには REDASH_DATABASE_URL, REDASH_REDIS_URL を指定する必要がありました。

# ファイルに直書きしたくない機密情報は secret として、パラメータストアから取得する
secrets:
  REDASH_DATABASE_URL: /copilot/redash/production/secrets/redash_database_url
  REDASH_REDIS_URL: /copilot/redash/production/secrets/redash_redis_url

もしすでに存在する PostgreSQL と Redis を利用するのであれば、以下でその URL をパラメータストアに登録できます。

copilot secret init --name redash_database_url
# => 対話で入力した値が /copilot/redash/production/secrets/redash_database_url に登録される
copilot secret init --name redash_redis_url
# => 対話で入力した値が /copilot/redash/production/secrets/redash_redis_url に登録される

しかし、PostgreSQL と Redis さえも AWS Copilot CLI で作成できる Addon というしくみがあり、まとめて管理できて便利なので次で紹介します。

Addon

これまで紹介した Manifest ファイルとは別で、普通の CloudFormation テンプレートを配置することで任意の AWS リソースを作成できます。

Environment の AddonService の Addon が存在しますが、今回は後者を採用します。
PostgreSQL や Redis は特定の Service の持ち物ではないので前者の方が適しているように思えますが、Environment が作成した VPC などの情報は後者でしか参照できないためです。

今回は代表して server の Addon として定義します。

PostgreSQL (RDS) のテンプレート
copilot/server/addons/postgres.yml
Parameters:
  App:
    Type: String
  Env:
    Type: String
  Name:
    Type: String

Resources:
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: !Sub '${App}-${Env}'
      DBSubnetGroupName: !Sub '${App}-${Env}'
      SubnetIds: !Split [ ',', { Fn::ImportValue: !Sub '${App}-${Env}-PrivateSubnets' } ]

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub '${App}-${Env}-postgres'
      GroupName: !Sub '${App}-${Env}-postgres'
      VpcId: { Fn::ImportValue: !Sub '${App}-${Env}-VpcId' }
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: { Fn::ImportValue: !Sub '${App}-${Env}-EnvironmentSecurityGroup' }

  DBInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Snapshot
    UpdateReplacePolicy: Snapshot
    Properties:
      AllocatedStorage: 50
      AllowMajorVersionUpgrade: true
      AutoMinorVersionUpgrade: true
      BackupRetentionPeriod: 30
      DBInstanceClass: db.t3.micro
      DBInstanceIdentifier: !Sub '${App}-${Env}'
      DBName: redash
      DBSubnetGroupName: !Ref DBSubnetGroup
      Engine: postgres
      EngineVersion: 14.6
      MasterUsername: redash
      # パスワードは事前にパラメータストアに置いておきます
      MasterUserPassword: !Sub '{{resolve:ssm:/copilot/${App}/${Env}/secrets/redash_database_password}}'
      MultiAZ: false
      PreferredBackupWindow: '18:00-19:00'
      PreferredMaintenanceWindow: 'Sun:19:00-Sun:20:00'
      StorageType: gp2
      StorageEncrypted: false
      VPCSecurityGroups:
        - !Ref DBSecurityGroup

  DBUrlParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub '/copilot/${App}/${Env}/secrets/redash_database_url'
      Type: String
      Value: !Sub 'postgresql://redash:{{resolve:ssm:/copilot/${App}/${Env}/secrets/redash_database_password}}@${DBInstance.Endpoint.Address}:${DBInstance.Endpoint.Port}/redash'
Redis (ElastiCache) のテンプレート
copilot/server/addons/redis.yml
Parameters:
  App:
    Type: String
  Env:
    Type: String
  Name:
    Type: String

Resources:
  RedisSubnetGroup:
    Type: AWS::ElastiCache::SubnetGroup
    Properties:
      CacheSubnetGroupName: !Sub '${App}-${Env}'
      Description: !Sub '${App}-${Env}'
      SubnetIds: !Split [ ',', { Fn::ImportValue: !Sub '${App}-${Env}-PrivateSubnets' } ]

  RedisSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub '${App}-${Env}-redis'
      GroupName: !Sub '${App}-${Env}-redis'
      VpcId: { Fn::ImportValue: !Sub '${App}-${Env}-VpcId' }
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 6379
          ToPort: 6379
          SourceSecurityGroupId: { Fn::ImportValue: !Sub '${App}-${Env}-EnvironmentSecurityGroup' }

  RedisCluster:
    Type: AWS::ElastiCache::CacheCluster
    Properties:
      AutoMinorVersionUpgrade: true
      ClusterName: !Sub '${App}-${Env}'
      Engine: redis
      EngineVersion: 7.0
      CacheNodeType: cache.t4g.micro
      CacheSubnetGroupName: !Ref RedisSubnetGroup
      NumCacheNodes: 1
      SnapshotRetentionLimit: 14
      SnapshotWindow: '17:00-21:00'
      VpcSecurityGroupIds:
        - !Ref RedisSecurityGroup

  RedisUrlParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub '/copilot/${App}/${Env}/secrets/redash_redis_url'
      Type: String
      Value: !Sub 'redis://${RedisCluster.RedisEndpoint.Address}:${RedisCluster.RedisEndpoint.Port}/0'

以下がポイントとなります。

  • Fn::ImportValue を使って Environment のスタックが出力した値を参照しつつリソースを作成する
  • 最後にその URL をパラメータストアに登録する
  • これによって Service は REDASH_DATABASE_URL, REDASH_REDIS_URL を知ることができる

また、AWS に構築しているということはデータソースとしたいほかの RDS への接続をしたいことがよくあると思います。そのための VPC ピアリングの設定も Addon で書くとよいでしょう。

VPC ピアリングのテンプレート
copilot/server/addons/vpc-peering.yml
Parameters:
  App:
    Type: String
  Env:
    Type: String
  Name:
    Type: String

Mappings:
  Redash:
    production:
      CidrBlock: xxx.xxx.xxx.xxx/16

  OtherDB:
    production:
      CidrBlock: xxx.xxx.xxx.xxx/16
      RouteTableId: rtb-xxxxxxxx
      SecurityGroupId: sg-xxxxxxxx
      VpcId: vpc-xxxxxxxx

Resources:
  VPCPeeringFromRedashToOtherDB:
    Type: AWS::EC2::VPCPeeringConnection
    Properties:
      VpcId: { Fn::ImportValue: !Sub '${App}-${Env}-VpcId' }
      PeerVpcId: !FindInMap [ OtherDB, !Ref Env, VpcId ]
      Tags:
        - Key: Name
          Value: !Sub '${App}-${Env}-rds-${Env}'

  RouteFromRedashToOtherDB:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: { Fn::ImportValue: !Sub '${App}-${Env}-PublicRouteTableID' }
      DestinationCidrBlock: !FindInMap [ OtherDB, !Ref Env, CidrBlock ]
      VpcPeeringConnectionId: !Ref VPCPeeringFromRedashToOtherDB

  RouteFromOtherDBToRedash:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !FindInMap [ OtherDB, !Ref Env, RouteTableId ]
      DestinationCidrBlock: !FindInMap [ Redash, !Ref Env, CidrBlock ]
      VpcPeeringConnectionId: !Ref VPCPeeringFromRedashToOtherDB

  IngressFromRedashToOtherDB:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !FindInMap [ OtherDB, !Ref Env, SecurityGroupId ]
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432
      SourceSecurityGroupId: { Fn::ImportValue: !Sub '${App}-${Env}-EnvironmentSecurityGroup' }

準備が整ったので以下でデプロイします。

copilot svc deploy -e production -n server # Addon も作られる
copilot svc deploy -e production -n scheduler
copilot svc deploy -e production -n scheduled-worker
copilot svc deploy -e production -n adhoc-worker
copilot svc deploy -e production -n worker

初回のみ、データベースの初期化 (テーブル作成) をします。

copilot svc exec -e production -n server -c 'bin/docker-entrypoint create_db'

おわりに

以上で AWS Copilot CLI で管理していける Redash を構築できました。

ブラウザ

ちなみに各 Service の cpu, memory は実際にレンティオで設定している値をそのまま載せました。Redash の CPU 消費は少なめですが、結果が大きいクエリを扱うにはある程度のメモリが必要なのでこの値に行き着いています。

ECS

ホスティングの情報があまりない Redash ですが、本記事がその手助けになれば幸いです。

おまけ: PostgreSQL のデータ移行

今回は新規構築ではなくリプレイスをしたため、旧 Redash が使っていた PostgreSQL のデータを移行する必要がありました。おまけとしてその手順も載せておきます。

  • 旧 DB のスナップショットを作成
  • スナップショットから DB インスタンスを立ち上げる
    • 新 DB と同じサブネットグループに配置する
  • データ移行
    • copilot svc exec -e production -n server して以下を実行
    • 一時的に server の storage.ephemeral を DB 容量の数倍にしておくことを推奨します
    # postgresql-client をインストール
    # See: https://www.postgresql.org/download/linux/ubuntu/
    apt update
    apt install lsb-release
    echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
    wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
    apt update
    apt install postgresql-client-<YOUR_POSTGRES_VERSION>
    
    # 旧 DB からダンプ
    pg_dump -h <OLD_DB_HOST> -U redash -d redash -f /tmp/redash.dump
    
    # 新 DB にリストア
    psql -h <NEW_DB_HOST> -U redash -d redash -f /tmp/redash.dump
    

採用情報

レンティオではエンジニアを募集しています。もし興味をお持ちいただけたらこちらもお目通しいただけると幸いです。

https://recruit.rentio.co.jp/engineer

https://www.rentio.jp/

脚注
  1. レンティオの本番環境をAWS Copilot/Fargate構成に切り替えた話 ↩︎

Discussion

Shunsuke ISHIKAWAShunsuke ISHIKAWA

参考になる記事をありがとうございます。
typoと思われる箇所がありましたので、コメントさせていただきました。

誤:copilot env init --name xxxx
正:copilot svc init --name xxxx