cdkでFargateを用いてRedashを設営した際にハマった箇所を書いてみた

BIツールを費用抑えめでマネージドに設営したかったので、cdk + Fargate + Redashという中々茨の道らしいものをやってみました。
2021.05.26

Athenaで集計する依頼を度々受けることが有り、事ある毎に依頼を受けるよりは「お好きにどうぞ」と作業環境を利用可能にする方が手間は省けます。問題は、IAMロールの発行が面倒な点です。

IAMロールをあれこれするコマンドを作っていたところ、「集計した結果がグラフ化できるといいね」という声があり、ならBIツールを立ててみるかと一念発起してみました。

コストを取って楽をするか、設置作業量をとって利用費に目を瞑るかの2択で、今後継続するなら断然後者だよねとcdkによるFargateを利用したRedash設営を試してみました。

念頭に置いておくべきこと

Redash設営に伴って、いくつか肝に命じておくことがあります。

  • 第一にserver、その後create_db、そしてschedulerとworker
  • healthcheckに使えるポイントがいくつかある
  • serverの受付Portが5000
  • ソースはgetredash/redashを利用する(setupにDockerfileは入ってない)

事前にわかるのはこの辺りまでです。

Dockerfile内でパーミッション指定を追加する

getredash/redashからDockerfileを拝借し、一行追記しておきます。これは、ローカルでfrontendをビルドした際に権限がなくてエラーとなる状態の対処です。

  RUN useradd -m -d /frontend redash
+ RUN chown -R 1001:1001 "/frontend/"
  USER redash

cdkでFargateのタスク及びサービスの作成

細かい変数の詳細は端折っていますが、大体以下のようになります。

    const repository = Repository.fromRepositoryName(this, `redashRepository`, 'redash/assets');
    const image = ContainerImage.fromEcrRepository(repository, `latest`);
    // server -> create_db -> scheduler/worker
    const services: ServiceName[] = ['server', 'create_db', 'scheduler', 'worker'];
    services.forEach(serviceName => {
      const securityGroup = serviceName === 'server' ? props.serverSecurityGroup :
                            serviceName === 'scheduler' ? props.schedulerSecurityGroup :
                            props.workerSecurityGroup;

      const logGroup = new LogGroup(this, `${serviceName}LogGroup`, {
        retention: RetentionDays.ONE_MONTH
      });

      const cluster = new Cluster(this, `${serviceName}Cluster`, {
        vpc: props.vpc
      });
      const taskDefinition = new FargateTaskDefinition(this, `${serviceName}TaskDefinition`, {
        cpu: context[serviceName].cpu,
        memoryLimitMiB: context[serviceName].memory,
        taskRole: taskRole,
      });

      const container = taskDefinition.addContainer(serviceName, {
        image: image,
        command: [serviceName],
        entryPoint: ['/app/bin/docker-entrypoint'],
        environment: {
          ...environment,
          REDASH_LOG_LEVEL: serviceName === 'server' ? 'INFO' : 'WARNING',  // ログレベルをサービスによって変える
        },
        secrets,
        memoryLimitMiB: context[serviceName].memory,
        logging: LogDriver.awsLogs({
          logGroup: logGroup,
          streamPrefix: 'ecs'
        })
      });
      const service = new FargateService(this, `${serviceName}Service`, {
        cluster: cluster,
        platformVersion: FargatePlatformVersion.VERSION1_4,
        taskDefinition: taskDefinition,
        desiredCount: context[serviceName].count,
        vpcSubnets: {
          subnetType: SubnetType.PRIVATE
        },
        securityGroup: securityGroup
      });
      if (serviceName === 'server') {
        container.addPortMappings({containerPort: 5000});
        albTargetGroup.addTarget(service);
      } else if (serviceName === 'worker') {
        container.addPortMappings({containerPort: 5000});
      }
    });

create_dbをサービスとして扱っている理由は以下の点です。

  • create_dbは再実行するとテーブル作成時に既存例外を利用して処理をスキップするため大事にはならない
  • 管理コンソール上からログを手軽に見たかった

無駄に実行することを防ぐため、一度実行した後に管理コンソール上からcreate_dbのタスク数を0にしておきます。

Redisを単ノードで追加する

複数ノードがある場合、Redashから接続ができず50xエラーとなっていました。指定次第では免れるかもしれませんが、とりあえずこのやり方で動作しました。

    const redisSubnetGroup = new CfnSubnetGroup(
      this,
      "RedashRedisClusterPrivateSubnetGroup",
      {
        description: `Redash Redis Cluster Private Subnet Group`,
        cacheSubnetGroupName: "redash",
        subnetIds: props.vpc.privateSubnets.map(function(subnet) {
          return subnet.subnetId
        })
      }
    );
    const redis = new CfnCacheCluster(
      this,
      `RedisCluster`,
      {
        engine: "redis",
        port: 6379,
        cacheNodeType: `cache.t3.small`,
        numCacheNodes: 1,
        clusterName: 'redash',
        vpcSecurityGroupIds: [props.cacheSecurityGroup.securityGroupId],
        cacheSubnetGroupName: redisSubnetGroup.cacheSubnetGroupName
      }
    );
    redis.addDependsOn(redisSubnetGroup);

ALBのヘルスチェックを極力伸ばす

Redashの立ち上がりにそこそこ時間がかかるため、初期値だと立ち上がり前にヘルスチェックが時間切れでエラーになります。cdkのドキュメントを見る限り、ALBは最大60秒まで伸ばせるため最大値をとりました。

    const albTargetGroup = new ApplicationTargetGroup(this, 'ALBTargetGroup', {
      deregistrationDelay: Duration.seconds(10),
      healthCheck: {
        path: '/ping',
        port: '5000',
        protocol: Protocol.HTTP,
        timeout: Duration.seconds(60),
        interval: Duration.seconds(70),
        healthyHttpCodes: "200-302"
      },
      protocol: ApplicationProtocol.HTTP,
      port: 5000,
      targetType: TargetType.IP,
      vpc: props.vpc
    });

pathが/pingなのは、テストケースから拝借しました。

あとがき

すでにAirflow設置用のスタックが存在していたため修正は手直し程度でしたが、Redash独自の挙動はCloudWatchLogsを見ていてもそこまで把握できるものでもなく、設定を変えては動作させるの繰り返しとなりました。

すでにYAMLを利用した例が多数上がっていたことは認知していましたが、折角だしcdkでやりたいと思って四苦八苦してみました。とりあえず動いてはいるものの、手直しが必要な点は多数ありそうなので、運用しつつの調整としたいと思います。