EC2 Image Builderを用いたRedashの運用改善

ogp

こんにちは、SRE部の谷口(case-k)です。

本記事では、EC2 Image Builderを使いRedashの運用改善を行った事例をご紹介します。運用しているRedashについてご紹介し、その後、Redashの運用課題に対してEC2 Image Builderでどのように解決したかTipsも踏まえご紹介します。

余談ですが全国どこでも働けるようになったので沖縄に住めています(感謝!)

https://press-tech.zozo.com/entry/20210118_zozotechpress-tech.zozo.com

目次

運用しているRedashの紹介

まず運用しているRedashの役割やインフラ構成、クエリ実行の流れ、起動時の処理についてご紹介します。

役割

ZOZOテクノロジーズでは配信基盤をインハウス化して自社で開発しています。メルマガやLINEなど複数のチャンネルに対して配信しています。

techblog.zozo.com

Redashの主な役割としては「分析」と「監視」です。

分析では配信施策の状況のモニタリングや施策の効果検証をしています。また、Redashではクエリ実行結果に基づいた監視も可能なので配信状況などの異常検知やデータ連携遅延などの他サービスの監視も行っています。

インフラ構成

ZOZOテクノロジーズで運用しているRedashは、公式に提供されているRedash AMIをベースにしています。Redash AMIからインスタンスを起動すると、Web UIをホスティングするRedashサーバー、クエリの実行を担うRedashワーカーが立ち上がり、Redashを利用できるようになリます。

クエリの数が増えてもRedashワーカーがオートスケールできるよう、ALB配下にはRedash AMIから起動したEC2インスタンスを配置しています。可用性を高めるため、フルマネージドでマルチAZ構成可能なAWSのAurora(PostgreSQL)とElastiCache(Redis)を参照するようにしています。

Redashは社内ツールであり、他サービスの監視ツールとしても使われています。そのため、冗長構成により可用性の高い運用を行っています。

なお、CloudForamtionについてはこちらにまとめました。

redash_infra

クエリ実行の流れ

クエリ実行の流れを説明します。

まず、実行するクエリはWeb UIをホスティングしているRedashサーバーからElastiCache(Redis)に保存されます。

クエリの実行はRedashワーカーで行われるため、RedashワーカーはElastiCache(Redis)に保存されたクエリを取得し、BigQueryなどデータソースに対してクエリを実行します。

そして、クエリの実行結果はAurora(PostgreSQL)に書き込まれます。書き込まれたクエリの実行結果はRedashサーバーより読み出されWeb UIで確認できます。なお、クエリの実行は分散タスクキューのCeleryによって非同期に行われます。

redash_query_execute

クエリ実行の流れは以下の記事が参考になりました。

speakerdeck.com

EC2インスタンス起動時の処理

EC2インスタンスの起動時にはミドルウェアの接続先を変更するための処理をしています。

Redash AMIは起動時にDocker Composeでコンテナイメージをビルドします。コンテナイメージをビルドするとRedashサーバーやワーカー、ミドルウェアであるPostgreSQL、Redisコンテナが立ち上がります。その際に立ち上がるRedashサーバーやワーカーが参照するミドルウェアの接続先はの環境設定ファイルに定義されています。Redash AMIをそのまま使うと、Docker Composeで立ち上げたPostgreSQL、Redisの接続先は環境設定ファイルに定義されたものが使われます。なお、Redash AMIの起動時のビルド処理はユーザーデータには定義されておりません。

今回行いたいことはインフラ構成にて説明したような冗長構成です。EC2インスタンス2台の冗長構成にするため、ミドルウェアをEC2インスタンスの外で管理する必要があります。AWSのAurora(PostgreSQL)とElastiCache(Redis)を参照するようEC2インスタンスの起動時に環境設定ファイルを書き換えてコンテナイメージをビルドする必要があります。

そのため、ユーザーデータでAWSのCLIを使えるようライブラリをインストールし、CLIでAWSシークレットマネージャー管理下の秘密情報を取得します。秘密情報としてAuroraやElastiCacheのユーザー情報やデータソースの復号化に必要な「REDASH_COOKIE_SECRET」や「REDASH_SECRET_KEY」を管理しています。そして、取得した秘密情報とCloudFormationで作ったリソースに基づいて環境設定ファイルを生成し、コンテナイメージをビルドします。

Redash AMIには起動時にコンテナイメージのビルド処理が組み込まれています。この処理はユーザーデータで定義した処理より前に実行されるため、Redash AMIの古い環境設定ファイルに基づいてコンテナイメージをビルドします。古いコンテナイメージだとミドルウェアの接続先が正しくないため、ユーザーデータでは、Redash AMIで作られたコンテナを落としてから、新しい環境設定ファイルに基づいてビルドしています。

UserData:
  Fn::Base64: !Sub |
    #!/bin/bash -e
    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/RDS/User  | jq -r .SecretString)
    RedashPassword=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/RDS/Password | jq -r .SecretString)
    cat <<EOF > /opt/redash/env
      PYTHONUNBUFFERED=0
      REDASH_LOG_LEVEL=INFO
      POSTGRES_PASSWORD=${!RedashPassword}
      REDASH_COOKIE_SECRET=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/CookieSecret | jq -r .SecretString)
      REDASH_SECRET_KEY=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/SecretKey | jq -r .SecretString)
      REDASH_REDIS_URL=redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0
      REDASH_DATABASE_URL=postgresql://${!RedashUsername}:${!RedashPassword}@${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}/${!RedashUsername}
      REDASH_FEATURE_EXTENDED_ALERT_OPTIONS=true
    EOF
    sudo docker-compose -f /opt/redash/docker-compose.yml down
    sudo docker-compose -f /opt/redash/docker-compose.yml up -d --build

Redashの運用課題

EC2インスタンス起動時にRedash AMI側の古い環境設定ファイルに基づいたビルドと、ユーザーデータで定義した新しい環境設定ファイルに基づいたビルドをしています。2つのビルドが同時に実行されることでRedashワーカーが正常に動かず、クエリ実行結果が返ってこない事象が確認できました。そのため、Redashワーカーを正常に動かすために、EC2インスタンス初回起動時のみ手動でEC2インスタンスの再起動する運用をしていました。せっかくクエリの個数に応じてオートスケールできる仕組みにしたのに活用できずにいました。また、データ分析の他に配信状況やデータ連携の遅延などの監視にも使われているため、監視ツールとしての役割に不安がありました。

EC2 Image Builderによる課題解決

前述のRedashの運用課題はコンテナイメージのビルド処理を制御すれば改善できるため、事前にAMIを作ることで解決できます。

手動でカスタムAMIを作る場合、Redashのバージョンアップやその他リソースの変更の度にカスタムAMIを作る必要があります。加えて、運用における属人化を防ぐ意味でも全てCloudFormationで管理したい思いがありました。

そのため、CloudFormationで管理可能で、カスタムAMIの手動運用が不要なEC2 Image Builderを使うことにしました。

EC2 Image Builderの紹介

EC2 Image BuilderとはカスタムAMIの作成を自動化するサービスです。

CloudFormationによる表現も可能で、一連のAMI作成をコード管理できます。CloudFormation管理にすることで、CloudFormationで作られたリソースに基づいたAMIの生成が可能となり属人的な運用の回避にも繋がります。

ここではCloudFormationを使った各リソースのTipsをご紹介します。

各リソースのTips

EC2 Image BuilderでカスタムAMIを作るときには4つの要素があります。

  • コンポーネント
  • レシピ
  • インフラストラクチャ
  • イメージパイプライン

各リソースについてCloudFormationを使いながらご紹介します。なお、「イメージパイプライン」はRedashの運用改善では使っていません。

ここで紹介する、EC2 Image BuilderによるAMI生成の全体図は次の通りです。

image_builder_1

Icons made by Freepik from www.flaticon.com

事前準備

事前準備としてソースイメージと、EC2 Image Builderに必要なIAMを定義します。

注意点はソースイメージに指定できるものはAWSが指定するマネージドなAMIもしくは、SSMがインストールされたカスタムAMIに限られている点です。そのため、公式に提供されているRedashのAMIをソースイメージに指定できなかったので、Ubuntuのイメージに必要なモジュールをインストールしました。

そして、EC2 Image Builderで必要なIAM権限は「EC2InstanceProfileForImageBuilder」と「AmazonSSMManagedInstanceCore」です。

また、EC2 Image Builderの内部処理としてSSMを呼び出しています。エラーについてもSSMのオートメーションページに出力されます。後述しますがSSMに出力されるエラーはデバッグが容易ではないので注意が必要です。 image_builder_2

Icons made by Freepik from www.flaticon.com
EC2ImageBuilderForRedash:
  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/EC2InstanceProfileForImageBuilder'
      - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'

InstanceProfile:
  Type: AWS::IAM::InstanceProfile
  Properties:
    InstanceProfileName: ImageBuilderInstanceProfile
    Roles:
      - !Ref EC2ImageBuilderForRedash

docs.aws.amazon.com

コンポーネント

コンポーネントはカスタムAMI作成に必要な手順を定義したリソースです。

カスタムAMIに必要なモジュールを定義した手順に沿ってインストールします。更新したコンポーネントを反映したい場合はCloudFormation反映時に「Version: 1.0.0」の部分のバージョン番号を変更して適用します。

image_builder_3

Icons made by Freepik from www.flaticon.com
Component:
  Type: AWS::ImageBuilder::Component
  Properties
    Data: |
      name: InstallApache
      description: InstallApache
      schemaVersion: 1.0
      phases:
        - name: build
          steps:
            - name: UpdateOS
              action: UpdateOS
            - name: RedashDir
              action: ExecuteBash
              inputs:
                commands:
                  - mkdir /opt/redash
            - name: docker-install
              action: ExecuteBash
              inputs:
                commands:
                  - sudo apt-get update
                  - sudo apt-get install -y \
                    apt-transport-https \
                    ca-certificates \
                    curl \
                    gnupg-agent \
                    software-properties-common
                  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
                  - sudo apt-key fingerprint 0EBFCD88
                  - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
                  - sudo apt-get update
                  - sudo apt-get install -y docker-ce docker-ce-cli containerd.io
            - name: docker-compose-install
              action: ExecuteBash
              inputs:
                commands:
                  - sudo apt-get update
                  - sudo curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
                  - sudo chmod +x /usr/local/bin/docker-compose
                  - sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
            - name: aws-cli-install
              action: ExecuteBash
              inputs:
                commands:
                  - 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
    Name: redash-ami-component
    Platform: Linux
    # update version when fix the component
    Version: 1.0.0

docs.aws.amazon.com

レシピ

レシピはソースとなるイメージとコンポーネントを紐付けるリソースです。

先ほど述べましたが、ソースイメージとして指定できるのはAWSが指定するマネージドなAMI、もしくはSSMがインストールされたカスタムAMIです。

ここで定義したレシピに基づいてAMIが生成されます。なお、コンポーネントを変えた場合はレシピのバージョンも更新して反映します。 image_builder_4

Icons made by Freepik from www.flaticon.com
Recipe:
  Type: AWS::ImageBuilder::ImageRecipe
  Properties:
    Components:
      - ComponentArn: !Ref Component
    Name: redash-ami-recipe
    # parentImage only accept aws managed image or custom ami installed ssm. so can not use redash ami
    ParentImage: arn:aws:imagebuilder:ap-northeast-1:aws:image/ubuntu-server-18-lts-x86/2020.9.23
    Version: 1.0.0

docs.aws.amazon.com

インフラストラクチャ

インフラストラクチャはイメージのビルドからテストまでの実行環境を定義するリソースです。

「TerminateInstanceOnFailure」を「false」に設定すると、処理の失敗時にインスタンスを終了せずに済みます。そのため、SSMのエラー内容が不十分な際に活用できます。

なお、AMIをビルドする環境はインターネットへ接続できる環境である必要があるので、サブネットを定義する際には注意が必要です。

image_builder_5

Icons made by Freepik from www.flaticon.com
InfrastructureConfiguration:
  Type: AWS::ImageBuilder::InfrastructureConfiguration
  Properties:
    InstanceProfileName: !Ref InstanceProfile
    InstanceTypes: []
    Name: redash-ami-infrastructure-configuration
    SecurityGroupIds: []
    TerminateInstanceOnFailure: True

docs.aws.amazon.com

カスタムAMIの生成

生成するRedashのイメージは次の通りです。

まず、レシピとイメージを定義しRedashのカスタムAMIを自動生成します。すると、ビルド用のインスタンスが起動、終了した後にテスト用のインスタンスが起動します。

所用時間として30〜60分ほどかかるので時間を短縮したい場合は「ImageTestsEnabled」を「false」に設定する手段もあります。 image_builder_6

Icons made by Freepik from www.flaticon.com
RedashAmiImage:
  Type: AWS::ImageBuilder::Image
  Properties:
    ImageRecipeArn: !Ref Recipe
    InfrastructureConfigurationArn: !Ref InfrastructureConfiguration
    ImageTestsConfiguration:
      ImageTestsEnabled: true
      TimeoutMinutes: 60

docs.aws.amazon.com

イメージパイプライン

イメージパイプラインはカスタムAMIの生成をスケジューリング実行するためのリソースです。

今回は使いませんでしたが、先ほど紹介したカスタムAMIの生成処理を定期実行する際に利用できます。他にもRedashの定期的なバージョンアップなどを自動化する際に活用できます。 image_builder_7

Icons made by Freepik from www.flaticon.com
Type: AWS::ImageBuilder::ImagePipeline
Properties: 
  Description: String
  DistributionConfigurationArn: String
  EnhancedImageMetadataEnabled: Boolean
  ImageRecipeArn: String
  ImageTestsConfiguration: 
    ImageTestsConfiguration
  InfrastructureConfigurationArn: String
  Name: String
  Schedule: 
    Schedule
  Status: String
  Tags: 
    Key : Value

docs.aws.amazon.com

EC2 Image Builderの利点・欠点

EC2 Image Builderを実際に利用し、そこから得られた利点と欠点を紹介します。

利点

カスタムAMIの手動運用が不要になる

カスタムAMIの手動運用が不要になったのは喜ばしい効果です。

特に頻繁にバージョンアップが必要なケースでは有益です。Redashもそうですが、バージョンアップ関連の処理に利用範囲を拡げていきたいです。

リソースをコードで管理できる

カスタムAMIなどリソースがコード管理されてないと属人的な運用になってしまうので、CloudFormationを使いコードで管理できるのもメリットです。Terraformでもサポートされています。

EC2インスタンスの起動時間を短縮できる

事前に必要なモジュールがインストール済みのAMIを使えるので、EC2インスタンスの起動が早くなります。

EC2インスタンスのユーザーデータでインストールするにはモジュールが多すぎる場合に有効活用できます。例えばEC2インスタンスで稼働させているDigdagのワーカーなどにも活用できます。

欠点

エラーログの調査が大変

エラーログを調査する際に、SSMのエラーログだけでは具体的にどこで落ちたのかわかりにくいです。

原因を特定するためにはインフラストラクチャで「TerminateInstanceOnFailure」を「false」に設定し、EC2インスタンス内からログの調査を実施する必要があります。

AMI生成までの時間が長い

上述の通り、ビルドからデプロイまで長い場合だと60分ほど時間がかかります。これは、再掲の内容ですが、ビルド用のインスタンスが起動、終了した後にテスト用のインスタンスが起動するためです。

合わせて欠点の1つ目に記載したエラーログ調査の難解さもあり、必然的に開発ライフサイクルが長くなります。

EC2 Image Builderが担う範囲の検討

前章の利点・欠点であげたように、RedashのカスタムAMIの手動運用が不要になり、運用課題を解決できました。

一方で、エラーログの調査方法とAMI生成までの時間に関しては懸念が残ります。失敗時のログの調査とAMI生成までの時間を考慮すると、リソースを変更する度にEC2 Image Builderの更新が必要になる運用は避けたいです。

そのため、EC2 Image Builderではライブラリのインストールのみ実施することにしました。環境設定ファイルやdocker-compose.ymlの生成、ビルドはEC2インスタンスのユーザーデータで行っています。

今後、Redashのバージョンアップを自動化する際には、EC2 Image Builderで動的に生成すべきですが、現時点の運用ではこのような役割分担にしました。

UserData:
  Fn::Base64: !Sub |
    #!/bin/bash -e
    RedashUsername=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/RDS/User  | jq -r .SecretString)
    RedashPassword=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/RDS/Password | jq -r .SecretString)
    cat <<EOF > /opt/redash/env
      PYTHONUNBUFFERED=0
      REDASH_LOG_LEVEL=INFO
      POSTGRES_PASSWORD=${!RedashPassword}
      REDASH_COOKIE_SECRET=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/CookieSecret | jq -r .SecretString)
      REDASH_SECRET_KEY=$(aws secretsmanager get-secret-value  --region ${AWS::Region} --secret-id Redash/SecretKey | jq -r .SecretString)
      REDASH_REDIS_URL=redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0
      REDASH_DATABASE_URL=postgresql://${!RedashUsername}:${!RedashPassword}@${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}/${!RedashUsername}
      REDASH_FEATURE_EXTENDED_ALERT_OPTIONS=true
    EOF
    cat <<EOF > /opt/redash/docker-compose.yml
      version: "2"
      x-redash-service: &redash-service
        image: redash/redash:8.0.0.b32245
        env_file: /opt/redash/env
        restart: always
      services:
        server:
          <<: *redash-service
          command: server
          ports:
            - "5000:5000"
          environment:
            REDASH_WEB_WORKERS: 4
        scheduler:
          <<: *redash-service
          command: scheduler
          environment:
            QUEUES: "celery"
            WORKERS_COUNT: 1
          logging:
            driver: awslogs
            options:
              awslogs-region: ap-northeast-1
              awslogs-group: redash_scheduler_logs
              awslogs-stream: redash_scheduler
        scheduled_worker:
          <<: *redash-service
          command: worker
          environment:
            QUEUES: "scheduled_queries,schemas"
            WORKERS_COUNT: 1
        adhoc_worker:
          <<: *redash-service
          command: worker
          environment:
            QUEUES: "queries"
            WORKERS_COUNT: 2
        nginx:
          image: redash/nginx:latest
          ports:
            - "80:80"
          depends_on:
            - server
          links:
            - server:redash
          restart: always
    EOF
    sudo docker-compose -f /opt/redash/docker-compose.yml up -d --build

まとめ

Redashは監視の役割も担っていたので、可用性の低い状態で他のサービスの監視をすることに不安がありました。その不安を払拭するためにも、EC2 Image Builderを導入したことで初回起動時に発生していたRedashの運用課題を解決でき、監視ツールとして可用性を高められました。

また、分析ツールとしてもクエリの実行数に応じてオートスケールが可能になりました。加えて、CloudFormationを使いコードとして管理できるので、カスタムAMIの運用負荷だけではなく属人的な運用を防ぐ意味でも役立ちそうです。

実際に試してみることでEC2 Image Builderの仕様も理解できました。同時に運用まで経験することでデバッグのやりにくさや、ビルドからデプロイまでの所要時間の課題感にも気づけました。

そこから、バージョンアップなど定期的に更新が必要な場合に相性が良い仕組みだという知見も得られました。

さいごに

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!

https://tech.zozo.com/recruit/tech.zozo.com

カテゴリー