case-kの備忘録

日々の備忘録です。データ分析とか基盤系に興味あります。

Cloud FormationでAWSリソースを使ったRedash環境を構築してみる

Cloud FormationでAWSリソースを使ったRedash環境を構築してみました。Redashの開発環境の際必要となったことを備忘録として残せたらと思います。

Redashとは

役割

Redashはデータ分析を行うためのダッシュボードです。BigQueryなどのデータソースを追加し、クエリの実行結果に基づいたダッシュボードを作ることできます。また、クエリ実行結果に基づいたアラート通知を行うことが可能です。
redash.io

構成要素

Redashの構成要素は次の通りです。

  • Nginx: Webアプリのリバースプロキシ
  • Gunicorn: WSGI対応のサーバ。RedashのWebアプリを動かす

Nginx+gunicorn構成でFlaskを使う[ローカル環境編] - Qiita

  • Celery: 分散タスクキュー。クエリの非同期実行を行う

django+Celeryによる非同期処理について - Qiita

  • Redis: Cerelyのメッセージブローカー。

https://aws.amazon.com/jp/redis/Redis_Streams/

  • PostgreSQL: データベース。各種情報の保持
  • Supervisor:プロセス管理ツール

「Redash遅くね?」と言われた時に確認すること - Qiita

クエリ実行フロー

Redashは RedisとPostgresを使っています。クエリ処理の流れとしては次の図がわかりやすいです。
f:id:casekblog:20200606142028p:plain:w500
一歩踏み込む Redash - Speaker Deck

作りたいもの

次の構成でRedash環境を作ってみます。データベースはAWSのAurora PostgreSQLとElasticache Redisを使います。ドメインAWSの別アカウント(本番環境)で使っているドメインサブドメインを使って開発環境を作ります。
f:id:casekblog:20200608120413p:plain:w500

Redash AMI
Setting up a Redash Instance

BigQuery接続方法
BigQuery

Redashの注意点

Redash環境作るにあたり注意点をまとめました。

環境変数の変更

Redashが参照するデータベースなど環境変数ファイルを書き換える必要があります。環境変数ファイルを変更した場合、再度コンテナを起動させる必要がありますが、次に記載されている通りコンテナの再起動では不十分です。

「 Once you updated the configuration, restart all services (docker-compose up -d, running docker-compose restart won’t be enough as it won’t read changes to env file).
To test email configuration, docker-compose run --rm server manage send_test_mail」

Setting up a Redash Instance

試しに「docker-compose down 」から「docker-compose up -d --build 」を行ってみたところ、Redash UIから投げたクエリが返りませんでした。
UserDataの初期化は成功しており、ワーカーコンテナも起動してたのでなかなか謎でした。
対応として初回に再起動「sudo shutdown -r now」を行ったところうまく行きました。EC2 Image Builderを使って環境変数ファイル変更後に動的にAMIを作るでも良いかもしれません。
EC2 Image Builderで作ったAMI IDを動的に取得して、AutoScalingグループの起動設定を更新する - Qiita

          {
            echo PYTHONUNBUFFERED=0
            echo REDASH_LOG_LEVEL=INFO
            echo POSTGRES_PASSWORD=${!RedashPassword}
            echo REDASH_COOKIE_SECRET=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-cookie-secret | jq -r .SecretString)
            echo REDASH_SECRET_KEY=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-secret-key | jq -r .SecretString)
            echo REDASH_REDIS_URL=redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0
            echo REDASH_DATABASE_URL=postgresql://${!RedashUsername}:${!RedashPassword}@${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}/${!RedashUsername}
            echo REDASH_FEATURE_EXTENDED_ALERT_OPTIONS=true
            } >> /opt/redash/env
           sudo shutdown -r now
Redashのバージョンアップ

Redashのバージョンアップは段階的に行う必要があります。次のように参照するAMIを変更して段階的にマイグレーションを行います。
f:id:casekblog:20200608235503p:plain
今回バージョン4.0.1からバージョン8.00にあげた際は次の手順で段階的にマイグレーションを行いました。
1. 最新(バージョン8.00)のAMIを起動
Setting up a Redash Instance

2. バージョンを変更してビルド

sudo vi /opt/redash/docker-compose.yml
 redash/redash:8.0.0.b32245  → redash/redash:4.0.1.b4038
sudo docker-compose -f /opt/redash/docker-compose.yml down
sudo docker-compose -f /opt/redash/docker-compose.yml up -d --build

2. マイグレーション対象のデータをロード(redash/redash:4.0.1.b4038)

3. マイグレーション

sudo docker-compose stop server scheduler scheduled_worker adhoc_worker
sudo docker-compose run --rm server manage db upgrade
sudo docker-compose up -d
sudo docker  ps -a

4. バージョンを変更してビルド、マイグレーション処理を繰り返す

sudo vi /opt/redash/docker-compose.yml
 redash/redash:4.0.1.b4038 → redash/redash:5.0.0.b4754
sudo docker-compose -f /opt/redash/docker-compose.yml down
sudo docker-compose -f /opt/redash/docker-compose.yml up -d --build
sudo docker-compose stop server scheduler scheduled_worker adhoc_worker
sudo docker-compose run --rm server manage db upgrade
sudo docker-compose up -d
sudo docker  ps -a
Redashの秘密情報

Redash AMIの環境変数ファイルにある「REDASH_COOKIE_SECRET」や「 REDASH_SECRET_KEY」がないとデータソースの追加や参照ができなくなります。Redashはバージョンアップも頻繁にあるので注意が必要です。
旧Redash(V2) から 新Redash(V8) にアップデートを行いました

Cloud Formation

ここからはCloud Formationを使ってAWS上にRedash環境を作ります。

ネットワーク

まずはネットワーク環境を作ります。

パラメータ

Cloud Formationで扱うパラメータを定義します。

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  GlobalPrefix:
    Type: 'String'
    Default: '<GlobalPrefix>'
  GlobalEnvironment:
    Type: 'String'
    Default: '<GlobalEnvironmen>'
  VPCCidrBlock:
    Type: 'String'
    Default: '10.0.0.0/16'
  IPCidrBlockPublicAZ1:
    Type: 'String'
    Default: '10.0.1.0/24'
  IPCidrBlockPublicAZ2:
    Type: 'String'
    Default: '10.0.2.0/24'
  IPCidrBlockPrivateAZ1:
    Type: 'String'
    Default: '10.0.3.0/24'
  IPCidrBlockPrivateAZ2:
    Type: 'String'
    Default: '10.0.4.0/24'
  IPCidrBlockAllow1
    Type: 'String'
    Default: '<Ip Address>'
  IPCidrBlockAllow2
    Type: 'String'
    Default: '<Ip Address>'
  AZ1:
    Type: 'AWS::EC2::AvailabilityZone::Name'
    Default: ''
  AZ2:
    Type: 'AWS::EC2::AvailabilityZone::Name'
    Default: ''
  SSHKeyName :
    Type: 'String'
    Default: ''
  NativeDomain:
    Type: 'String'
    Default: '<Domain Name>'
VPC

ネットワーク領域を定義します。IPアドレスの範囲を示すときは「CIDR表記」もしくは「サブネットマスク表記」のいずれかを用います。VPCのCIDRは「10.0.0.0/16」とします。

  ## VPC Definition --------------------------------------------------------------------------------
  EC2VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      Tags:
       - Key: Name
         Value: VPC
DHCP

DHCP (Dynamic Host Configuration Protocol) は、 IPv4ネットワークにおいて通信用の基本的な設定を自動的に行うためのプロトコルです。
DHCP オプションセット - Amazon Virtual Private Cloud

  ## DHCP Definition --------------------------------------------------------------------------------
  EC2DHCPOptions:
    Type: 'AWS::EC2::DHCPOptions'
    Properties:
      DomainName: !Sub '${AWS::Region}.compute.internal'
      DomainNameServers:
        - 'AmazonProvidedDNS'
      Tags:
        - Key: 'Name'
          Value: !Sub ${AWS::StackName}-dhcp-options
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
  EC2VPCDHCPOptionsAssociation:
    Type: 'AWS::EC2::VPCDHCPOptionsAssociation'
    Properties:
      DhcpOptionsId: !Ref EC2DHCPOptions
      VpcId: !Ref EC2VPC
サブネット

サブネットはVPCをさらに分割したネットワークです。インターネットからアクセスできるパブリックサブネットとアクセスできないプライベートサブネットを作ります。可用性を高めるためマルチAZ構成(パブリックサブネット×2, プライベートサブネット×2, )で作ります。AZ(アベイラビリティゾーン)は各地域にあるリージョン(データセンター群)をさらに分割したものです。

  • パブリックサブネット
  ## Subnet Definition --------------------------------------------------------------------------------
  EC2SubnetPublicAZ1:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref IPCidrBlockPublicAZ1
      MapPublicIpOnLaunch: true
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2SubnetPublicAZ1
  EC2SubnetPublicAZ2:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Ref IPCidrBlockPublicAZ2
      MapPublicIpOnLaunch: true
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2SubnetPublicAZ2
  • プライベートサブネット
  EC2SubnetPrivateAZ1:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref IPCidrBlockPrivateAZ1
      MapPublicIpOnLaunch: false
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2SubnetPrivateAZ1
  EC2SubnetPrivateAZ2:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Ref IPCidrBlockPrivateAZ2
      MapPublicIpOnLaunch: false
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2SubnetPrivateAZ2

ルートテーブル

ネットワークにデータを流すために「ルーティング情報」の設定が必要です。ルートテーブルはVPCとサブネットの紐付けを行います。ルートテーブルはサブネットごとに設定することができます。

  ## RouteTable Definition --------------------------------------------------------------------------------
  EC2RouteTablePublicAZ1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2RouteTablePublicAZ1
  EC2RouteTablePublicAZ2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2RouteTablePublicAZ2
  EC2RouteTablePrivateAZ1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2RouteTablePrivateAZ1
  EC2RouteTablePrivateAZ2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EC2VPC
      Tags:
        - Key: Name
          Value: EC2RouteTablePrivateAZ2
  EC2SubnetRouteTableAssociationPublicAZ1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref EC2RouteTablePublicAZ1
      SubnetId: !Ref EC2SubnetPublicAZ1
  EC2SubnetRouteTableAssociationPublicAZ2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref EC2RouteTablePublicAZ2
      SubnetId: !Ref EC2SubnetPublicAZ2
  EC2SubnetRouteTableAssociationPrivateAZ1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref EC2RouteTablePrivateAZ1
      SubnetId: !Ref EC2SubnetPrivateAZ1
  EC2SubnetRouteTableAssociationPrivateAZ2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref EC2RouteTablePrivateAZ2
      SubnetId: !Ref EC2SubnetPrivateAZ2

インターネットゲートウェイ

パブリックサブネットをインタネットに接続するためにはインターネットゲートウェイが必要です。インターネットゲートウェイは自分のネットワークにインタネット回線を引き込む役割があります。

  ## Internet Gateway Definition --------------------------------------------------------------------------------
  EC2InternetGateWay:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: EC2InternetGateWay
  EC2GatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref EC2InternetGateWay
      VpcId: !Ref EC2VPC
  EC2InternetGatewayRouteAZ1:
    Type: "AWS::EC2::Route"
    DependsOn:
      - EC2GatewayAttachment
    Properties:
      RouteTableId: !Ref EC2RouteTablePublicAZ1
      GatewayId: !Ref EC2InternetGateWay
      DestinationCidrBlock: 0.0.0.0/0
  EC2InternetGatewayRouteAZ2:
    Type: "AWS::EC2::Route"
    DependsOn:
      - EC2GatewayAttachment
    Properties:
      RouteTableId: !Ref EC2RouteTablePublicAZ2
      GatewayId: !Ref EC2InternetGateWay
      DestinationCidrBlock: 0.0.0.0/0
NATゲートウェイ

DBサーバ(片方向)からの接続だけを許すためにNATゲートウェイを作ります。NATゲートウェイはインターネットにアクセスするために、パブリックサブネットに配置します。それぞれEIPが必要となります。

  ## Nat Gateway Definition --------------------------------------------------------------------------------
  EC2EIPForNatGatewayAZ1:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: 'vpc'
      Tags:
        - Key: Name
          Value: EC2EIPForNatGatewayAZ1
  EC2NatGatewayAZ1:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt EC2EIPForNatGatewayAZ1.AllocationId
      SubnetId: !Ref EC2SubnetPublicAZ1
      Tags:
        - Key: Name
          Value: EC2NatGatewayAZ1
  EC2EIPForNatGatewayAZ2:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: 'vpc'
      Tags:
        - Key: Name
          Value: EC2EIPForNatGatewayAZ2
  EC2NatGatewayAZ2:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt EC2EIPForNatGatewayAZ2.AllocationId
      SubnetId: !Ref EC2SubnetPublicAZ2
      Tags:
        - Key: Name
          Value: EC2NatGatewayAZ2
  EC2InternetNatGatewayRouteAZ1:
    Type: 'AWS::EC2::Route'
    Properties:
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref EC2NatGatewayAZ1
      RouteTableId: !Ref EC2RouteTablePrivateAZ1
  EC2InternetNatGatewayRouteAZ2:
    Type: 'AWS::EC2::Route'
    Properties:
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref EC2NatGatewayAZ2
      RouteTableId: !Ref EC2RouteTablePrivateAZ2

開発環境に本番環境のサブドメインを適用する

AWSの本番環境(別アカウント)で取得して使っているドメイン(親)のサブドメイン(子)を開発環境のRedashに割り当てます。

Route53

Route53はAWSドメインネームシステムでドメインの取得やゾーンを作ることができます。Route53を使って開発環境にゾーンを作ります。作られたゾーンはCloud Formation間で共有できるようにExportします。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NativeDomain:
    Type: 'String'
    Default: '<~.com.>'
Resources:
  Route53HostedZoneNativeDomain:
    Type: "AWS::Route53::HostedZone"
    DeletionPolicy: 'Retain'
    Properties:
      Name: !Sub dev.${NativeDomain}
Outputs:
  OutputNativeDomainHostedZoneID:
    Value: !Ref Route53HostedZoneNativeDomain
    Export:
      Name: !Join [':', [!Ref 'AWS::StackName', 'native-domain', 'hosted-zone-id'] ]
  OutputNativeDomainNameServers:
    Value: !Join [',', !GetAtt Route53HostedZoneNativeDomain.NameServers]
AWS Certificate Manager(ACM)

AWS Certificate Manager (ACM) はAWS ベースのウェブサイトとアプリケーション用のパブリック SSL/TLS 証明書の複雑な作成と管理を処理します。RedashをSSL対応させるためにはACM証明書が必要です。

ACM DNS検証

次のようにしてACMDNS認証リクエストを投げます。リクエストを投げると検証中となり、CNAMEレコードが表示されます。
ACM証明書発行をDNS検証で行う - サーバーワークスエンジニアブログ

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NativeDomain:
    Type: 'String'
    Default: '<Domain Name>'
Resources:
  CertificateManagerCertificateRedash:
    Type: 'AWS::CertificateManager::Certificate'
    Properties:
      DomainName: !Sub redash.dev.${NativeDomain}
      ValidationMethod: 'DNS'
Outputs:
  OutputCertificateManagerRedashDomainArn:
    Value: !Ref CertificateManagerCertificateRedash
    Export:
      Name: !Join [':', [!Ref 'AWS::StackName', 'redash-domain', 'arn'] ]
CNAMEレコードを追加

CNAMEをCloud Formationに入力して適用します。認証が完了するとAWSコンソールのゾーンにCNAMEレコードが追加され、ACM証明書が発行されます。
【ドメイン】DNSレコード設定の各レコードの意味を教えてください。|ヘルプサポート | ドメイン取るならお名前.com

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  DNSRecordPairRedashDevDomain:
    Type: 'CommaDelimitedList'
    Default: '<Cname Name >,_<Cname Value>'
Resources:
  Route53RecordSetCNAMEACMRedashDevDomain:
    Type: 'AWS::Route53::RecordSet'
    Properties:
      HostedZoneId: !ImportValue hosted-zone:native-domain:hosted-zone-id
      Name: !Select [0, !Ref DNSRecordPairRedashDevDomain]
      ResourceRecords:
        - !Select [1, !Ref DNSRecordPairRedashDevDomain]
      TTL: '60'
      Type: 'CNAME'
NSレコードの追加

NSレコードはゾーン情報を管理するネームサーバーのサーバー名を定義するレコードです。NSレコードはドメインの委任に関する情報を管理しています。NSレコードはゾーンカットの親側と子側の双方に設定する必要があります。例えばjpがexample.jpを委任している時、jpとexample.jpゾーンの双方で設定します。今回本番環境で取得したドメイン(親)のサブドメインを開発環境(子)に割り当てたいので、開発環境のNSレコードを本番環境のゾーンにも追加します。NSレコードは開発環境にゾーン作成後コンソールから取得します。NSレコードをCloud Formationに入力して本番環境に適用します。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NativeDomain:
    Type: 'String'
    Default: '<Domain Name>.'
  RedashDevNSRecords:
    Type: 'CommaDelimitedList'
    Default: 'ns-.net., ns-.com., ns-.org., ns-.co.uk.'
Resources:
  Route53RecordSetAliasRedashElb:
    Type: 'AWS::Route53::RecordSet'
    Properties:
      HostedZoneId: !ImportValue hosted-zone:native-domain:hosted-zone-id # <Domain Name>.
      Name: !Sub dev.${NativeDomain}
      Type: 'NS'
      TTL: '900'
      ResourceRecords:
      - !Select [0, !Ref RedashDevNSRecords]
      - !Select [1, !Ref RedashDevNSRecords]
      - !Select [2, !Ref RedashDevNSRecords]
      - !Select [3, !Ref RedashDevNSRecords]

AWSリソースを使ったRedash

AWSリソースを使ったRedash環境を構築します。秘密情報の管理はSecrets Managerを使います。
CloudFormationテンプレートに秘密情報を渡す方法 - ZOZO Technologies TECH BLOG

AutoScaling

サーバーが死んだら自動復旧(オートヒーリング)させたいので、AWSの オートスケーリング機能を利用します。次の記事の「スケーリングはせずインスタンス数の維持に利用する」に該当する処理を行います。
AWS再入門2018 Amazon EC2 Auto Scaling編 | Developers.IO

  ## AutoScaling Definition  --------------------------------------------------------------------------------
  AutoScalingGroupForRedash:
    Type: 'AWS::AutoScaling::AutoScalingGroup'
    Properties:
      VPCZoneIdentifier:
        - !Ref EC2SubnetPrivateAZ1
        - !Ref EC2SubnetPrivateAZ2
      LaunchConfigurationName: !Ref LaunchConfigurationForRedash
      MinSize: '1'
      MaxSize: '3'
      TargetGroupARNs:
        - !Ref ElasticLoadBalancingV2TargetGroupExternalRedash
      DesiredCapacity: '1'
      Tags:
        - Key: Name
          Value: EC2RedashAutoScalingServerPrivateAZ1AZ2
          PropagateAtLaunch: true
    UpdatePolicy:
      AutoScalingReplacingUpdate:
        WillReplace: true
  EC2RedashInstanceIAMRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/SecretsManagerReadWrite'
  EC2RedashInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Path: "/"
      Roles:
        - !Ref EC2RedashInstanceIAMRole
  LaunchConfigurationForRedash:
    Type: AWS::AutoScaling::LaunchConfiguration
    Properties:
      ImageId: "ami-060741a96307668be"
      IamInstanceProfile: !Ref EC2RedashInstanceProfile
      SecurityGroups:
        - !Ref EC2SecurityGroupAutoScalingServerRedash
      InstanceType: 'm5.xlarge'
      KeyName: !Ref SSHKeyName
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          rm /opt/redash/env
          curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
          sudo rm /var/lib/dpkg/lock*
          sudo dpkg --configure -a
          sudo apt update
          sudo apt install python -y
          sudo python get-pip.py
          sudo pip install awscli
          sudo apt install jq -y
          RedashUsername=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-user  | jq -r .SecretString)
          RedashPassword=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-password | jq -r .SecretString)
          {
            echo PYTHONUNBUFFERED=0
            echo REDASH_LOG_LEVEL=INFO
            echo POSTGRES_PASSWORD=${!RedashPassword}
            echo REDASH_COOKIE_SECRET=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-cookie-secret | jq -r .SecretString)
            echo REDASH_SECRET_KEY=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-secret-key | jq -r .SecretString)
            echo REDASH_REDIS_URL=redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0
            echo REDASH_DATABASE_URL=postgresql://${!RedashUsername}:${!RedashPassword}@${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}/${!RedashUsername}
            echo REDASH_FEATURE_EXTENDED_ALERT_OPTIONS=true
            } >> /opt/redash/env
           sudo shutdown -r now
  EC2SecurityGroupAutoScalingServerRedash:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Activity security group
      VpcId: !Ref EC2VPC
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref EC2SecurityGroupStepServerRedash
          IpProtocol: 'tcp'
          FromPort: 22
          ToPort: 22
        - SourceSecurityGroupId: !Ref EC2SecurityGroupALBExternalRedash
          IpProtocol: 'tcp'
          FromPort: 80
          ToPort: 80
      Tags:
        - Key: Name
          Value: EC2SecurityGroupAutoScalingServerRedash
Application Load Balance(ALB)

ALB(Application Load Balancer)とは、Amazon.comが提供するAWSAmazon Web Services)と呼ばれるシステムの一部で、Webサービスに発生する負荷を分散するロードバランシングサービスです。

  • サブネット

ALBにはパブリックサブネットを紐付けます。

  ## ALB Definition  --------------------------------------------------------------------------------
  ElasticLoadBalancingV2LoadBalancerExternalRedash:
    Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
    Properties:
      Name: !Sub external-${GlobalPrefix}-redash
      Scheme: 'internet-facing'
      SecurityGroups:
        - !Ref EC2SecurityGroupALBExternalRedash
      Subnets:
        - !Ref EC2SubnetPublicAZ1
        - !Ref EC2SubnetPublicAZ2
      Tags:
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
  • ターゲットグループ

ターゲットグループは次のようにして定義します。ALBはターゲットグループのインスタンスに対して負荷を分散します。

  ElasticLoadBalancingV2TargetGroupExternalRedash:
    Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
    Properties:
      Port: 80
      Protocol: 'HTTP'
      VpcId: !Ref EC2VPC
      HealthCheckPath: '/login'
      HealthCheckPort: 'traffic-port'
      HealthCheckProtocol: 'HTTP'
      HealthCheckTimeoutSeconds: 2
      HealthyThresholdCount: 2
      TargetGroupAttributes: [
           { "Key" : "stickiness.enabled", "Value" : "true" },
           { "Key" : "stickiness.type", "Value" : "lb_cookie" },
           { "Key" : "stickiness.lb_cookie.duration_seconds", "Value" : "86400" }
      ]
      Matcher:
        HttpCode: '200-399'
      UnhealthyThresholdCount: 2
      TargetType: 'instance'
      Tags:
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment

HealthCheckPath: '/login'
Redash EC2 instance behind ELB or CloudFront distribution - #3 by hernangarcia - Self Hosted Redash Support - Redash Discourse
【基礎から学ぶ】ELBのスティッキーセッションについてまとめてみた - サーバーワークスエンジニアブログ

  • リスナー

リスナーとは設定したプロトコルとポートを使用して接続リクエストをチェックするプロセスです。リスナーは次のように定義します。ACM証明書を使いSSL化させます。開発環境のドメインのゾーンにAレコードを追加します。
AレコードとはIPアドレスの関連づけを定義するレコードです。Aレコードのドメインから名前解決を行うことができます。もし「www.example.com」でWEBサイトからアクセスしたいならAレコードは「www.example.com」とします。
【ドメイン】DNSレコード設定の各レコードの意味を教えてください。|ヘルプサポート | ドメイン取るならお名前.com

  ElasticLoadBalancingV2ListenerExternalRedashHTTPS:
    Type: 'AWS::ElasticLoadBalancingV2::Listener'
    Properties:
      Certificates:
        - CertificateArn: !ImportValue acm:redash-domain:arn
      DefaultActions:
        - TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroupExternalRedash
          Type: 'forward'
      LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancerExternalRedash
      Port: 443
      Protocol: 'HTTPS'
  Route53RecordSetAliasRedashElb:
    Type: 'AWS::Route53::RecordSet'
    Properties:
      AliasTarget:
        DNSName: !GetAtt ElasticLoadBalancingV2LoadBalancerExternalRedash.DNSName
        HostedZoneId: !GetAtt ElasticLoadBalancingV2LoadBalancerExternalRedash.CanonicalHostedZoneID
      HostedZoneId: !ImportValue hosted-zone:native-domain:hosted-zone-id
      Name: !Sub redash.dev.${NativeDomain}.
      Type: 'A'
  EC2SecurityGroupALBExternalRedash:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupName: !Sub ${GlobalPrefix}-${GlobalEnvironment}-redash-alb-security-group
      GroupDescription: 'alb security group for redash'
      VpcId: !Ref EC2VPC
      SecurityGroupIngress:
        - CidrIp: !Ref IPCidrBlockAllow1
          FromPort: 443
          ToPort: 443
          IpProtocol: 'tcp'
        - CidrIp: !Ref IPCidrBlockAllow2
          FromPort: 443
          ToPort: 443
          IpProtocol: 'tcp'
Aurora PostgreSQL

AWSのAuroraを使います。セキュリティグループの範囲としては、踏み台サーバとALBで作られたインスタンスのみからとし、プライベートサブネットに配置します。

  ## Aurora PostgreSQL Definition  --------------------------------------------------------------------------------
  RDSDBClusterParameterGroupForRedash:
    Type: 'AWS::RDS::DBClusterParameterGroup'
    Properties:
      Description: 'rds cluster parameter group for digdag'
      Family: 'aurora-postgresql10'
      Parameters:
        timezone: 'Asia/Tokyo'
      Tags:
        - Key: 'Name'
          Value: !Sub ${GlobalPrefix}-${GlobalEnvironment}-rds-cluster-parameter-group-digdag
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
  RDSDBParameterGroupForRedash:
    Type: 'AWS::RDS::DBParameterGroup'
    Properties:
      Description: 'rds parameter group for redash'
      Family: 'aurora-postgresql10'
      Tags:
        - Key: 'Name'
          Value: !Sub ${GlobalPrefix}-${GlobalEnvironment}-rds-parameter-group-redash
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
  RDSDBSubnetGroupNameForRedash:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupDescription: 'rds subnet group for redash'
      DBSubnetGroupName: !Sub ${GlobalPrefix}-${GlobalEnvironment}-redash-rds-subnet-group
      SubnetIds:
        - !Ref EC2SubnetPrivateAZ1
        - !Ref EC2SubnetPrivateAZ2
  RDSDBClusterForRedash:
    Type: 'AWS::RDS::DBCluster'
    DeletionPolicy: 'Snapshot'
    Properties:
      AvailabilityZones:
        - !Ref AZ1
        - !Ref AZ2
      BackupRetentionPeriod: 35
      DBClusterParameterGroupName: !Ref RDSDBClusterParameterGroupForRedash
      DBSubnetGroupName: !Ref RDSDBSubnetGroupNameForRedash
      Engine: 'aurora-postgresql'
      EngineVersion: '10.7'
      MasterUsername: '{{resolve:secretsmanager:redash-master-user:SecretString}}'
      MasterUserPassword: '{{resolve:secretsmanager:redash-master-password:SecretString}}'
      Port: 5432
      PreferredBackupWindow: '21:00-21:30' # JST 05:00-05:30
      PreferredMaintenanceWindow: 'Thu:20:00-Thu:20:30' # JST FRI 04:00-04:30
      StorageEncrypted: true
      Tags:
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
      VpcSecurityGroupIds:
        - !Ref EC2SecurityGroupRDSRedash
  IAMRoleRDSEnhancedMonitoring:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: 'Allow'
          Principal:
            Service: 'monitoring.rds.amazonaws.com'
          Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole'
  RDSForRedash:
    Type: 'AWS::RDS::DBInstance'
    DeletionPolicy: 'Delete'
    Properties:
      AllowMajorVersionUpgrade: false
      AutoMinorVersionUpgrade: false
      CopyTagsToSnapshot: true
      DBClusterIdentifier: !Ref RDSDBClusterForRedash
      DBInstanceClass: 'db.t3.medium'
      Engine: 'aurora-postgresql'
      MonitoringInterval: 15
      MonitoringRoleArn: !GetAtt IAMRoleRDSEnhancedMonitoring.Arn
      PreferredMaintenanceWindow: 'Thu:20:00-Thu:20:30' # JST FRI 04:00-04:30
      PubliclyAccessible: false
      Tags:
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
      DBParameterGroupName: !Ref RDSDBParameterGroupForRedash
  EC2SecurityGroupRDSRedash:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupName: !Sub ${GlobalPrefix}-${GlobalEnvironment}-redash-rds-security-group
      GroupDescription: 'rds security group for redash'
      VpcId: !Ref EC2VPC
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref EC2SecurityGroupStepServerRedash
          FromPort: 5432
          ToPort: 5432
          IpProtocol: 'tcp'
        - SourceSecurityGroupId: !Ref EC2SecurityGroupAutoScalingServerRedash
          FromPort: 5432
          ToPort: 5432
          IpProtocol: 'tcp'
ElastiCache Redis

AWSの ElastiCacheを使います。セキュリティグループの範囲としては、踏み台サーバとALBで作られたインスタンスのみからとし、プライベートサブネットに配置します。

  ## ElastiCache Redis Definition  --------------------------------------------------------------------------------
  ElastiCacheSubnetGroupForRedash:
    Type: 'AWS::ElastiCache::SubnetGroup'
    Properties:
      CacheSubnetGroupName: !Sub ${GlobalPrefix}-${GlobalEnvironment}-redash-redis-subnet-group
      Description: 'redis subnet group for redash'
      SubnetIds:
        - !Ref EC2SubnetPrivateAZ1
        - !Ref EC2SubnetPrivateAZ2
  ElastiCacheParameterGroupForRedash:
    Type: 'AWS::ElastiCache::ParameterGroup'
    Properties:
      CacheParameterGroupFamily: 'redis4.0'
      Description: 'Redash Redis'
  ElasticacheClusterForRedash:
    Type: 'AWS::ElastiCache::CacheCluster'
    Properties:
      ClusterName: 'redash-redis'
      Engine: 'redis'
      EngineVersion: '4.0.10'
      AutoMinorVersionUpgrade: true
      CacheNodeType: 'cache.t2.micro'
      NumCacheNodes: 1
      CacheParameterGroupName: !Ref ElastiCacheParameterGroupForRedash
      PreferredAvailabilityZone: !Ref AZ1
      CacheSubnetGroupName: !Ref ElastiCacheSubnetGroupForRedash
      VpcSecurityGroupIds:
        - !GetAtt EC2SecurityGroupRedashRedis.GroupId
      Port: 6379
      Tags:
        - Key: 'Name'
          Value: !Sub ${GlobalPrefix}-${GlobalEnvironment}-redash
        - Key: 'Scope'
          Value: !Ref GlobalEnvironment
  EC2SecurityGroupRedashRedis:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupName: !Sub ${GlobalPrefix}-${GlobalEnvironment}-redash-redis-security-group
      GroupDescription: 'redis security group for redash'
      VpcId: !Ref EC2VPC
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref EC2SecurityGroupStepServerRedash
          FromPort: 6379
          ToPort: 6379
          IpProtocol: 'tcp'
        - SourceSecurityGroupId: !Ref EC2SecurityGroupAutoScalingServerRedash
          FromPort: 6379
          ToPort: 6379
          IpProtocol: 'tcp'
踏み台サーバ

踏み台サーバを作ります。UserDataではRedashユーザをつくります。

  ## Step Server Definition  --------------------------------------------------------------------------------
  EC2RedashStepServerPublicAZ1:
    Type: AWS::EC2::Instance
    DependsOn:
      - EC2InternetGatewayRouteAZ1
      - EC2SubnetRouteTableAssociationPublicAZ1
      - RDSForRedash
    Properties:
      InstanceType: t3.small
      SubnetId: !Ref EC2SubnetPublicAZ1
      ImageId: ami-07f4cb4629342979c
      IamInstanceProfile: !Ref EC2RedashInstanceProfile
      KeyName: !Ref SSHKeyName
      SecurityGroupIds:
        - !Ref EC2SecurityGroupStepServerRedash
      Tags:
        - Key: Name
          Value: EC2RedashStepServerPublicAZ1
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          sudo apt update
          sudo apt install postgresql-client-common
          sudo apt install postgresql-client -y
          sudo apt install python -y
          sudo curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
          sudo python get-pip.py
          sudo pip install awscli
          sudo apt install jq -y
          RedashMasterUsername=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-master-user  | jq -r .SecretString)
          RedashMasterPassword=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-master-password | jq -r .SecretString)
          RedashUsername=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-user  | jq -r .SecretString)
          RedashPassword=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id redash-password | jq -r .SecretString)
          echo ${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}:*:${!RedashMasterUsername}:${!RedashMasterPassword} >> ~/.pgpass
          chmod 600 ~/.pgpass
          psql -U ${!RedashMasterUsername} -h ${RDSDBClusterForRedash.Endpoint.Address} -d postgres -c "CREATE ROLE ${!RedashUsername} WITH LOGIN PASSWORD '${!RedashPassword}'"
          psql -U ${!RedashMasterUsername} -h ${RDSDBClusterForRedash.Endpoint.Address} -d postgres -c "GRANT ${!RedashUsername} TO ${!RedashMasterUsername}"
          psql -U ${!RedashMasterUsername} -h ${RDSDBClusterForRedash.Endpoint.Address} -d postgres -c "CREATE DATABASE ${!RedashUsername} OWNER ${!RedashUsername}"
  EC2EIPForStepServer:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: 'vpc'
      InstanceId: !Ref EC2RedashStepServerPublicAZ1
  EC2SecurityGroupStepServerRedash:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Activity security group
      VpcId: !Ref EC2VPC
      SecurityGroupIngress:
        - CidrIp: !Ref IPCidrBlockAllow1
          IpProtocol: 'tcp'
          FromPort: 22
          ToPort: 22
        - CidrIp: !Ref  IPCidrBlockAllow2
          FromPort: 22
          ToPort: 22
          IpProtocol: 'tcp'
      Tags:
        - Key: Name
          Value: EC2SecurityGroupStepServerRedash

なかなか長くなってしまいました。以上となります。