AWS Fargateで並行処理をした話


吉岡です。11月に開催されるRubyWorld Conferenceに登壇することが決まりました。弊社が提供する「Redmine」のクラウドサービス「My Redmine」のインフラストラクチャーを、Rubyと最新のクラウド技術を活用した構成へと再構築したことについて発表します。ぜひ会場までお越しください!

さて、今日のブログはAWSのFargateを利用して、RailsのDB作成とマイグレーションを複数サイトで同時実行したことについての話です。

目的

AWSのFargateというサービスを利用して、複数のサイト(複数のDB)に対して同時にバッチ処理を実行します。 今回はサンプルとして rails db:create と rails db:migrate を実行してみます。

前提条件

以下は、事前に準備

手順

  1. コンテナイメージの作成(今回は既存のイメージを使用)
  2. AWS ECSのクラスターとタスク定義の作成
  3. タスクの実行(複数同時実行)
  4. DBの確認

作業

1. コンテナイメージ

今回は既存のイメージを使用します。
https://hub.docker.com/_/redmine

ただし、既存のイメージだとDB作成の処理がないので、rake db:create の処理を追加したDocker imageを作ります。

以下のファイルを参考に起動時にENTORYPOINTで実行される docker-entorypoint.sh にDB作成の処理を追加します。 https://github.com/docker-library/redmine/blob/53046fd6e8e9557cf2c6c73bcf76af7a5bd14e96/4.0/alpine/docker-entrypoint.sh (rake db:migration の前に追加する)

docker-entorypoint.sh

if [ "$1" != 'rake' -a -z "$REDMINENODB_CREATE" ]; then
    rake db:create
fi

修正した、docker-entorypoint.sh を読み込んだDocker imageを作成します。

Dockerfile

FROM redmine:4.0.4-alpine

COPY docker-entrypoint.sh / RUN chmod +x /docker-entrypoint.sh

ビルド

docker build -t redmine-test --no-cache .

ビルドが終了したらローカルで動作確認をします。

# SQLで検証
docker run -d -p 3000:3000 redmine-test
# PostgreSQL(RDS)で検証
docker run -d -p 3000:3000 -e  REDMINE_DB_POSTGRES=[rds-endpoint] -e REDMINE_DB_USERNAME=postgres -e REDMINE_DB_PASSWORD=[postgres-password] redmine-test

動作確認ができましたら、以下のサイトを参考にECRへDocker imageをプッシュします。 https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/ECR_AWSCLI.html (またはdockerhubでも大丈夫です)

2. AWS ECSのクラスターとタスク定義の作成

続いて、Docker imageを動かすためのECSのクラスターとタスク定義をAWS CloudFormationを利用して一気に作ります。 (CloudFormationの詳細説明は省きます。)

まずは、以下のyamlファイルを作成します。 Parameters のDefaultの値は適宜変更してください。

fargate-template.yml

WSTemplateFormatVersion: '2010-09-09'
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
    - Label:
        default: 'Service Size'
      Parameters:
      - DesiredCount
      - MaxCapacity
      - MinCapacity
      - MemorySize
      - CpuSize
    - Label:
        default: 'Task Enviroment'
      Parameters:
      - RedmineDatabasePostgres
      - RedmineDatabase
      - RedmineDatabaseUser
      - RedmineDatabasePassword
Parameters:
  DesiredCount:
    Type: Number
    Default: 1
  MaxCapacity:
    Type: Number
    Default: 1
  MinCapacity:
    Type: Number
    Default: 1
  MemorySize:
    Type: String
    Default: '1024'
    AllowedValues: [ 1024, 2048, 3072, 4096, 5120, 6144, 7168,
      8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360,
      16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552,
      24576, 25600, 26624, 27648, 28672, 29696, 30720 ]
  CpuSize:
    Type: String
    Default: '256'
    AllowedValues: [256, 512, 1024, 2048, 3072, 4096]
  Image:
    Type: String
    Default: [ECR-path]
  ContainerPort:
    Type: String
    Default: '3000'
  ContainerName:
    Type: String
    Default: 'redmine-app'
  RedmineDatabasePostgres:
    Type: String
    Default: [RDS-Endpoint]
  RedmineDatabase:
    Type: String
    Default: [your-dbname]
  RedmineDatabaseUser:
    Type: String
    Default: [db-username]
  RedmineDatabasePassword:
    Type: String
    Default: [db-password]

Resources:

# Cluster ################################################################### Cluster: Type: 'AWS::ECS::Cluster' Properties: ClusterName: !Sub 'Redmine-${AWS::StackName}'

# Role ################################################################### TaskDefinitionRole: Type: 'AWS::IAM::Role' Properties: RoleName: !Sub 'TaskDefRole-${AWS::StackName}' Path: / AssumeRolePolicyDocument: Statement: - Action: 'sts:AssumeRole' Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'

# Log ################################################################### LogGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Join ['-', ['/esc/redmine', !Ref 'AWS::StackName']]

# Task ################################################################### Taskdefinition: Type: AWS::ECS::TaskDefinition Properties: Family: !Join ['', [!Ref 'AWS::StackName', -my-redmine]] NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ExecutionRoleArn: !Ref TaskDefinitionRole Cpu: !Ref CpuSize Memory: !Ref MemorySize ContainerDefinitions: - Name: !Ref ContainerName Essential: 'true' Image: !Ref Image LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref LogGroup awslogs-region: !Ref 'AWS::Region' awslogs-stream-prefix: ecs-redmine-app PortMappings: - ContainerPort: !Ref ContainerPort Environment: - Name: REDMINEDBPOSTGRES Value: !Ref RedmineDatabasePostgres - Name: REDMINEDBDATABASE Value: !Ref RedmineDatabase - Name: REDMINEDBUSERNAME Value: !Ref RedmineDatabaseUser - Name: REDMINEDBPASSWORD Value: !Ref RedmineDatabasePassword

コマンドラインで以下を実行します。

aws cloudformation create-stack \
  --stack-name redmine-test \
  --template-body file://fargate-template.yml \
  --capabilities CAPABILITY_NAMED_IAM

実行後、AWSコンソールからCloudFormationの画面でスタックの作成が完了するまで待ちます。redmine-testのステータスがCREATE_COMPLETEになっていることを確認します。

CloudFormationのスタック作成が完了していることを確認します。

3. タスクの実行(複数同時実行)

クラスターの作成とタスク定義が終了したら、いよいよタスクを実行してバッチ処理(DB作成とマイグレーション)を行います。 今回はローカルで実行しましたので、IAMで権限を作成して実行していますが、Lambdaで実行する実行する場合、access_key_id, secret_access_keyを設定するのではなく、ロールを設定して対応します。

また、今回は利用するDocker Imageが起動時にDBの作成とマイグレーションを実行するため、コマンドは特に指定をしません。(通常のバッチ処理ですとコマンドでバッチ処理のスクリプトを実行したりします。)

your-access-key-id, your-secret-access-key, subnet-id, security-group-id の値は適宜変更して実行してください。

fargate-migration.rb

#!/usr/local/bin/ruby
require 'aws-sdk'
require 'yaml'

client = Aws::ECS::Client.new(
  access_key_id: "your-access-key-id"
  secret_access_key: "your-secret-access-key"
)

# タスク実行時のデフォルトの設定情報を定義します。
task_prop = {
  cluster: "Redmine-redmine01",
  task_definition: "redmine01-my-redmine",
  launch_type: "FARGATE",
  overrides: {
    container_overrides: [
      {
          name: "redmine-app",
        environment: [ { name: "REDMINE_DB_DATABASE", value: "redmine01" }, ]
      }
    ]
  },
  network_configuration: {
    awsvpc_configuration: {
      subnets: [ "[subnet-id]" ],
      security_groups: ["[security-group-id]"],
      assign_public_ip: "DISABLED"
    },
  },
}

# 上記設定情報を利用して、Fargateを40個同時に立ち上げて、DBの作成とマイグレーションを行います。
# それぞれ、DBの情報を書き換えることによって、40個のDBを作成します。
40.times do |i|
  task_prop[:overrides][:container_overrides][0] = {
    name: "redmine-app",
    # ここで任意のコマンドを実行する
    # 今回は既存のイメージを使用するため、特にコマンドの上書きを行いませんでした。
    # command: ["echo 'success'"],
    environment: [ { name: "REDMINE_DB_DATABASE", value: "db-name-#{i}" } ]
  }
  resp = client.run_task(task_prop)
  p resp
end

上記必要箇所を書き換えて保存後に実行します。

ruby fargate-migration.rb

ECSのタスクを確認するとすると一気に複数のタスクが立ち上がって、DBの作成とマイグレーションが実行されます。

指定した数のタスクが起動していることを確認します。

注意点

  1. デフォルトではFargateは50個しか立ち上げられないので、それ以上利用する場合は制限解除の申請が必要です。
  2. AWS Batchと言うサービスでも同じことはできますが、今回は本番環境でECSを利用する場合、そのタスク定義を流用できると言う点でFargateの方が管理物が少なくて済むので魅力的でした。 (AWS Batchを利用した方が良いケースが多々あります。)
  3. デフォルトではFargate 起動タイプを使用するタスクのパブリックIPアドレスの数は50が上限となっています。50以上、同時に起動する場合は、制限解除の申請をするか、プライベートサブネットでNATゲートウェイを利用して、パブリックIPを使用しないように設定する必要があります。今回は制限の関係で40個同時起動しました。

4. DBの確認

PostgreSQLにアクセスして実際にDBが作成されているかどうか確認します。

psql -h [RDS-Endpoint] -U [USERNAME]

Before

postgres=> \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 rdsadmin  | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin
 template0 | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin          +
           |          |          |             |             | rdsadmin=CTc/rdsadmin
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(4 rows)

After

postgres=> \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
------------+----------+----------+-------------+-------------+-----------------------
 db-name-0  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-1  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-10 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-11 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-12 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-13 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-14 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 db-name-15 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 ...(省略)
 postgres   | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 rdsadmin   | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin
 template0  | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin          +
            |          |          |             |             | rdsadmin=CTc/rdsadmin
 template1  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
            |          |          |             |             | postgres=CTc/postgres

無事に40個のDBが作成され、マイグレーションも実行されてました。

Redmineが動いていることも確認します。

感想

これまでは Lambda でrake taskを実行するの難しかったですが、Docker imageを利用することによってRailsに依存したコマンドを簡単に実行できるようになりました。

今回はDB作成とマイグレーションを実行すると言うだけの簡単な処理でしたが、例えば rake task を利用してバッチ処理を作り、それを複数のサイトに実行した場合などに威力を発揮しそうです。


My Redmine

こちらの記事もオススメです!
AWS Summit Tokyo 2019に参加してきました(ついでにAWS認定試験を受けました)
幕張メッセで開催された「AWS Summit Tokyo」に参加。機械学習、サーバレス、コンテナに関する知識をアップデートしました。
当たり前と思われがちだけど、とても便利なRedmineのファイル添付機能の内側(Attachment 編)
Redmineのファイル添付機能は地味ですがとても便利機能。その内側を紹介します。
台湾最大オープンソースイベントCOSCUPは熱かった!
IT先進国台湾で開催されたオープンソースソフトウェアのイベントに参加してきました。
仕事をしながら深圳大学に1ヶ月留学してきた記録
中国の深圳大学に語学留学して、中国語を4週間学んできました。授業の様子と滞在中の生活を紹介。
「Redmine 4.1 新機能選抜総選挙」で紹介できなかった新機能 10選
redmine.tokyoでRedmine4.1の新機能16個を紹介。そのほか紹介できなかった便利な新機能10個をピックアップして紹介します。
ファーエンドテクノロジーからのお知らせ(2019/10/01更新)
2019年10月 オライリー本の全冊公開日のお知らせ(もくもく勉強会も同時開催)
ファーエンドテクノロジーが所蔵するオライリー本(全冊)公開日のご案内です。公開日には「もくもく勉強会」も同時開催します。
ファーエンドテクノロジーの岩石と吉岡がRubyWorld Conference2019に登壇します
11月7日(木)〜8日(金)に島根県松江市で開催される「RubyWorld Conference 2019」に弊社の岩石と吉岡が登壇します。
「FAREND NEWS」2019年第5号 発行
広報紙「FAREND NEWS」2019年第5号を発行しました。弊社サービスの運用・サポートに携わっているスタッフや弊社の取り組みをご紹介します。
RubyWorld Conference 2019 (11/7・8開催) にPlatinumスポンサーとして協賛
11月7日(木)〜8日(金)に島根県松江市で開催される「RubyWorld Conference 2019」にPlatinumスポンサーとして協賛します。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け