20200917のAWSに関する記事は11件です。

ECS FargateのログをFireLensを使ってCloudWatchとFirehoseの両方に送る

やりたいこと

ECS Fargateで動いているRailsのアプリケーションログをfirelens(カスタマイズしたfluent-bitのイメージ)を使ってCloudWatchと2つのFirehoseにそれぞれ送りたい。

①ECS Fargate -> Firehose -> S3 (foo_log) -> Glue -> Athena
②ECS Fargate -> Firehose -> S3 (bar_log) -> Glue -> Athena
③ECS Fargate -> CloudWatch (全てのログ)

にログを送りたいという想定。
(fooとbarはある特定の条件のログのみを抽出したいという意)

構成図

image.png
今回は青い部分の作業手順をまとめます。

作業手順

  1. リソース作成
  2. CloudWatchと2つのFirehoseに送るようにfluent-bitイメージをカスタマイズする
  3. log-routerコンテナを定義
  4. カスタマイズしたfluent-bitイメージでlog-routerコンテナを動かしてログを収集する

1. リソース作成

  • firehose(今回は各firehoseごとにIAMロールを作成する想定)
    • foo_firehose
      • foo_firehose_role
    • bar_firehose
      • bar_firehose_role
  • cloudwatch
  • S3

    • foo_bucket
    • bar_bucket
  • ECRリポジトリ(画像にはないがカスタマイズしたfluent-bitイメージを管理する)
    https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-create.html
    を参考に、カスタマイズしたfluent-bitのイメージを置くためのECRリポジトリを作成。

また各ロールに必要なポリシーをアタッチします。

ecs_task_role

  • firehoseにアクセスするためのPolicyをattach
ecs_task_firehose_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "firehose:PutRecordBatch",
            "Resource": [
                "arn:aws:firehose:{{region}}:{{account-id}}:deliverystream/foo-log",
                "arn:aws:firehose:{{region}}:{{account-id}}:deliverystream/bar-log"
            ]
        }
    ]
}
  • CloudWatchにアクセスするためのPolicyをattach arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy

foo_firehose_role

foo_firehoseがfoo_bucketにアクセスできるように、foo_firehose_roleにPolicyをattachする

foo_firehose_role_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",      
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [        
                "arn:aws:s3:::foo-bucket",
                "arn:aws:s3:::foo-bucket/*"         
            ]
        }
    ]
}

bar_firehose_role

bar_firehoseがbar_bucketにアクセスできるようbar_frehose_roleにPolicyをattachする

bar_firehose_role_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",      
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [        
                "arn:aws:s3:::bar-bucket",
                "arn:aws:s3:::bar-bucket/*"         
            ]
        }
    ]
}

2. CloudWatchと2つのFirehoseに送るようにfluent-bitイメージをカスタマイズする

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html
ここを読むと、firelensではAWSが提供しているAWS for Fluent Bitを使うこともできます。
しかしこれではCloudWatchとFirehoseの両方にログを送ることができないので、今回はfluent-bit.confをカスタマイズして加えたDockerイメージを使うようにします。

カスタマイズしたconfが以下の2つです。

docker/fluent-bit/fluent-bit.conf
[SERVICE]
    Flush 1
    Parsers_File fluent-bit-parsers.conf

# logというkeyの中身をデコードする
[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser rails
    # logの中身以外はいらないので消去する(container_idとか)
    Reserve_Data false

# ログにtagをつける
[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがfooのもののタグをfoo_logにする
    Rule    $name ^(foo)$ foo_log true

[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがbarのものはタグをbar_logにする
    Rule    $name ^(bar)$ bar_log true

# logの送信
# 全てのログ(rails-firelens-*というtagがついている)をCloudWatchに送る
[OUTPUT]
   Name                cloudwatch
   Match               rails-firelens-*
   region              {CloudWatchロググループのregion}
   log_group_name      {CloudWatchロググループの名前}
   # つけたいprefixを指定。ここではlatest/としている
   log_stream_prefix   latest/

# tagがfoo_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match foo_log
   region {foo_firehoseのregion}
   delivery_stream foo_firehose

# tagがbar_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match bar_log
   region {bar_firehoseのregion}
   delivery_stream bar_firehose
docker/fluent-bit/fluent-bit-parsers.conf
# ログがエンコードされてlogというkeyの中に入っているのでデコードするためのparser
# https://docs.fluentbit.io/manual/pipeline/parsers/decoders
[PARSER]
    Name         rails
    Format       json
    Time_Key     time
    Time_Format  %Y-%m-%dT%H:%M:%S %z
    # Command       |  Decoder  | Field | Optional Action   |
    # ==============|===========|=======|===================|
    Decode_Field_As    escaped     log

fluent-bitの公式ドキュメント
https://docs.fluentbit.io/manual/

基本的に公式ドキュメントに沿ってconfをカスタマイズしていきました。
ただドキュメントが全部英語なので、今回書いた部分だけ解説を残します。

解説

上から説明していきます。

[SERVICE]
    Flush 1
    Parsers_File fluent-bit-parsers.conf

[SERVICE]はログ全体の設定を定義するところです(多分)。

Flush 1ではドキュメントにあるようにログの出力間隔を指定しています。
https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/configuration-file

Flush
Set the flush time in seconds.nanoseconds. The engine loop uses a Flush timeout to define when is required to flush the records ingested by input plugins through the defined output plugins.

デフォルトだと5秒で、5秒だと例えばlog-routerコンテナが起動に失敗した時に、ログが出力される前にタスクがストップしてしまう、とかいうことがあったのでここでは1にしています。

Parsers_File fluent-bit-parsers.conf

ここではParserが書いてあるファイルパスを指定します。
独自でParserを作成した場合、このように別ファイルから読み込むようにするのがルール見たいです。Parserの解説は次にします。

ログの整形

Parser
https://docs.fluentbit.io/manual/pipeline/parsers/decoders を参考にして作成したParserのファイルです。
firelensは渡ってきたログに勝手に情報を付け加えるので、Parserに不要なログは切り落としてアプリケーションのログだけを収集するという設定をします。(実行はfluent-bit.confの[FILTER]で定義する)

fluent-bitは特にカスタマイズしないとこんな感じでログを送ります。

{
  "container_id": "3e638f00-c1a7-4794-b796-1d916dfa8cbc-1555792190",
  "container_name": "rails",
  "ecs_cluster": "{{cluster-name}}",
  "ecs_task_arn": "arn:aws:ecs:{{region}}:{{account-Id}}:task/{{task_id}}",
  "ecs_task_definition": "{{task-definition}}",
  "log": "{\"host\":\"xxxxxxxxx\",\"application\":\"xxxxxxxxxx\",\"environment\":\"xxxxxxx\",\"timestamp\":\"2020-01-01T00:00:00.00000Z\",\"level\":\"xxxx\",\"level_index\":x,\"pid\":xx,\"thread\":\"xxxxxx\",\"name\":\"xxxx\",\"message\":\"xxxxxx\"}

firelens側でcontainer_id とか container_name とか勝手につけてくれます。
そしてlogに実際にRailsアプリケーションが出したjsonのログがstringにエンコードされて入っています。
(今回はRailsアプリケーション側でもログはjsonで吐き出すように設定している)
これをjsonに戻すための設定を定義しているのが、このfluent-bit-parsers.confになります。

docker/fluent-bit/fluent-bit-parsers.conf
# ログがエンコードされてlogというkeyの中に入っているのでデコードするためのparser
# https://docs.fluentbit.io/manual/pipeline/parsers/decoders
[PARSER]
    Name         rails
    Format       json
    Time_Key     time
    Time_Format  %Y-%m-%dT%H:%M:%S %z
    # Command       |  Decoder  | Field | Optional Action   |
    # ==============|===========|=======|===================|
    Decode_Field_As    escaped     log

Name rails でこのParserに名前をつけています(自由です)。
一応命名規則としては、ログを吐き出したものの名前をつけるみたいで、今回扱うのはRailsが吐き出したログなのでrailsにしています。(ドキュメントではdockerになっている)

fluent-bit.confに戻ります。

docker/fluent-bit/fluent-bit.conf
# logというkeyの中身をデコードする
[FILTER]
    Name parser
    Match *
    Key_Name log
    Parser rails
    # logの中身以外はいらないので消去する(container_idとか)
    Reserve_Data false

ここで先ほど定義したParserを使って実行してね、という処理を書きます。
参考: https://docs.fluentbit.io/manual/pipeline/filters/parser

  • [FILTER]
    いろんな用途で使われるようで、用途ごとにNameを変えていきます。
    今回はParserを使いたいからName parserです。

  • Match *
    このFILTERに引っ掛けるログのタグを指定します。
    このタグについてはあとで解説します。
    *では全部のログがこのFILTERを通るという意味になります。

  • Key_Name log
    これがFILTERの対象にしたいログの中のKeyを指定するところですね。
    今回は log です。

  • Parser rails
    ここでどんなParserを使いたいか指定します。先ほど作成したrailsを指定。

  • Reserve_Data false
    これは Key_Name で指定した以外のKeyのデータはどうする?という意味になります。
    ここをfalseにすると指定した以外のKey、今回でいうとlogというKey以外は削除されます。
    よってログに実際のアプリケーションが出したログのみを出力することができます。
    もし、firelensが付与してくれたcontainer_idcontainer_nameとかも出力させたい!ということでしたらここをtrueにすれば出力されます。

ここまでがログをいい感じに整形する部分です。
次からはログを分割したい時の各設定です。
そこで必要な知識がさっき言ったログのタグです。

fluent-bitにおけるログのタグ

fluent-bit側で、ログの種類を分けるためにログにタグをつけることができます。
ただfluent-bitによって吐き出されたログがjsonの場合はそのタグの情報は見えません。
jsonでなければログは下のように出力されてタグも見えます。

[0] foo_log: [1598502349.876671400, {"host"=>"xxxxxxxxx", "timestamp"=>"2020-01-01T00:00:00.000000Z", "level"=>"xxxx", "level_index"=>0, "pid"=>00, "thread"=>"xxxxxxxxx", "name"=>"foo", "message"=>"xxxxxx"}}]

このfoo_logの部分がタグです。

また今回とっても大事なのが、firelensに入ってきたログにはデフォルトで
{container_name}-firelens-{task_id} というタグがつけられているということです。
今回だとrails-firelens-89caedfc-07fd-41f3-8239-5313a7d10ca7のようなタグが最初に全てのログにつけられています。
これをうまく使ってログの分割を行なっていきます。

ログのタグ付け

docker/fluent-bit/fluent-bit.conf
[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがfooのもののタグをfoo_logにする
    Rule    $name ^(foo)$ foo_log true

[FILTER]
    Name    rewrite_tag
    Match   *
    # nameがbarのものはタグをbar_logにする
    Rule    $name ^(bar)$ bar_log true

ここも[FILTER]ですがName rewrite_tagとなっています。
その名の通りここでタグの上書きを行います。

  • Rule $name ^(foo)$ foo_log true
    参考: https://docs.fluentbit.io/manual/pipeline/filters/rewrite-tag#rules
    ドキュメントの通りRuleでは$KEY REGEX NEW_TAG KEEPを記述しています。
    [FIRTER]は記述した順番に処理されるらしく、ここに来るログはすでにlogというkeyの中身(applicationが吐き出したログそのもの)がやってきます。
    今回は nameというkeyにfoobarという文字が入っているという想定です。

  • $name ^(foo)$ foo_log true
    これはnameというKeyの中身がfooならfoo_logというタグをつける、という意味になります。
    (^(foo)$は正規表現です)

  • true
    はタグをつけた後、タグをつける前のログをとっておくかどうか、を表します。
    具体的にいうと、もともとは rails-firelens-* というタグがついていたけれど、foo_log というタグを上書きしたので、元の rails-firelens-* のタグがついている方は取っておくか?という意味です。
    今回はこの後の用途にて必要になるのでtrueにして取っておくようにしています。
    false にすれば元のログは削除され foo_log とついたもののみ、残ります。

ここまでで、ログをいい感じに整形して、タグをつけるところまできました。
あとはタグを使って出力先を指定するだけです。

ログの出力

docker/fluent-bit/fluent-bit.conf
# logの送信
# 全てのログ(*-firelens-*というtagがついている)をCloudWatchに送る
[OUTPUT]
   Name                cloudwatch
   Match               rails-firelens-*
   region              {CloudWatchロググループのregion}
   log_group_name      {CloudWatchロググループの名前}
   # つけたいprefixを指定。ここではlatest/としている
   log_stream_prefix   latest/

# tagがfoo_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match foo_log
   region {foo_firehoseのregion}
   delivery_stream foo_firehose

# tagがbar_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match bar_log
   region {bar_firehoseのregion}
   delivery_stream bar_firehose

このように[OUTPUT]を複数かくと、複数の場所に出力されます。

  • CloudWatch
docker/fluent-bit/fluent-bit.conf
[OUTPUT]
    Name                cloudwatch
    Match               rails-firelens-*

今回はCloudWatchに「applicationが吐き出した全てのログ」を送りたいと考えています。
タグがfoo_log、bar_log、そうでないもの全てです。
ここで、さっきrails-firelens-*というタグのログを残しておいたのが生きてきます。
foo_logやbar_logというタグとは別にrails-firelens-*というタグがついた状態で全てのログが残っているので、このタグのログを送ってあげれば全てのログが送られることになります。ですのでMatch rails-firelens-*と書きます。

  • Firehose
docker/fluent-bit/fluent-bit.conf
# tagがfoo_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match foo_log
   region {foo_firehoseのregion}
   delivery_stream foo_firehose

# tagがbar_logのものをfirehoseに送る
[OUTPUT]
   Name firehose
   Match bar_log
   region {bar_firehoseのregion}
   delivery_stream bar_firehose

ここも難しくはなく、Matchで送りたいログのタグを指定して、delivery_streamに送るfirehose名をきます。

カスタマイズしたconfを配置するDockerfileを書く。

作成した、fluent-bit.conffluent-bit-parsers.conf をコンテナのルートに配置するようにDockerfileを書きます。(ルートでなくても問題ない)

docker/fluent-bit/Dockerfile.
FROM amazon/aws-for-fluent-bit:latest
COPY ./docker/fluent-bit/fluent-bit.conf /fluent-bit.conf
COPY ./docker/fluent-bit/fluent-bit-parsers.conf /fluent-bit-parsers.conf

3.log-routerコンテナを定義

ecs-task-definition.jsonはこのように書きます。

ecs-task-definition.json
{
  "containerDefinitions": [
    # railsコンテナを定義
    {
      "command": [
        "bundle",
        "exec",
        "unicorn",
        "-p",
        "3000",
        "-c",
        "/rails/config/unicorn_ecs.rb"
      ],
      "cpu": 0,
      "dnsSearchDomains": [],
      "dnsServers": [],
      "dockerSecurityOptions": [],
      "entryPoint": [],
      "environment": [],
      "essential": true,
      "image": "Railsサービスを動かすイメージを指定する"
      "links": [],
      # ここでrailsコンテナのログドライバーをfirelensにする
      "logConfiguration": {
        "logDriver": "awsfirelens"
      },
      "mountPoints": [],
      "name": "rails",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "volumesFrom": []
    },
    # log-routerコンテナを定義
    {
      "essential": true,
      "image": "カスタマイズしてbuild,pushしたECRにあるfluent-bitのイメージを指定",
      "name": "log-router",
      "environment": [],
      # カスタマイズしたconfをパス指定
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "config-file-type": "file",
          "config-file-value": "/fluent-bit.conf"
        }
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "log-router",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "latest"
        }
      },
      "memoryReservation": 50
    }
  ],
  "cpu": "2048",
  "taskRoleArn": "taskRoleArnを書く",
  "executionRoleArn": "taskExecutionRoleArnを書く",
  "family": "hoge",
  "memory": "4096",
  "networkMode": "awsvpc",
  "placementConstraints": [],
  "requiresCompatibilities": ["FARGATE"],
  "volumes": []
}

ここで使いたい fluent-bit.conf のパスを指定しています。

ecs-task-definition.json
# カスタマイズしたconfをパス指定
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": {
          "config-file-type": "file",
          "config-file-value": "/fluent-bit.conf"
        }

今回はルートに配置したので"config-file-value"/fluent-bit.conf です。
(ルート以外に配置した場合はそのパスを指定)
ここを指定しない場合はAWS側で /fluent-bit/etc/fluent-bit.conf にあるデフォルトのconfを見にいくようになっています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/using_firelens.html#firelens-example-firehose

重要
カスタム設定ファイルを使用する場合は、FireLens が使用するパスとは異なるパスを指定する必要があります。Amazon ECS では /fluent-bit/etc/fluent-bit.conf (Fluent Bit) と /fluentd/etc/fluent.conf (Fluentd) のファイルパスは予約されています。

4.カスタマイズしたfluent-bitイメージでlog-routerコンテナを動かしてログを収集する

後はfluent-bitのイメージをbuildし、ECRリポジトリへpushした後、ECS Fargateでコンテナを立ち上げます。
railsコンテナとlog-routerコンテナが正しく起動したのを確認し、

  • CloudWatch
  • S3
    • foo_bucket
    • bar_bucket

を見て、ログが正しく送られているかを確認します。

補足

特にCloudWatchでログのファイル名をよく見てみると、latest/rails-firelens-${ランダムな数字}となっているかと思います。
${ランダムな数字}{task_id}になっているはずです。
つまり、fluent-bit.confのCloudWatchの[OUTPUT]log_stream_prefix latest/と指定したprefixが使われ、{prefix}/{ログのタグ名}というファイル名になっているのです。
仮に Match rails-firelens-*ではなく、Match * にすると、全てのタグのログがCloudWatchに送られることになるので、このファイルの他に latest/foo_loglatest/bar_log というログファイルも作られます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSを使ってアプリケーションを公開する手順(7)Capistranoによる自動デプロイ

はじめに

AWSを使ってアプリケーションを公開する手順を記載していく。
この記事ではCapistranoを使ってデプロイ作業を自動化する。

Capistranoの導入

CapistranoはRubyで書かれており、Gemが公開されている。
この記事ではrailsにCapistranoを導入する手順を記載するが、PHPなどの別のフレームワークでも使用できるらしい。

Capistrano関連のGemをインストールする

Gemfileを以下のように編集する。

Gemfile
group :development, :test do
  gem 'capistrano'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano3-unicorn'
end

ターミナルで以下のコマンドを実行し、Gemfileを読み込む。

bundle install

以下のコマンドを実行し、Capistranoの関連ファイルを生成する。

bundle exec cap install

以下のファイルが生成される。各ファイルの詳細は後述する。

  • Capfile
  • config/deploy.rb
  • config/deploy/production.rb
  • config/deploy/staging.rb

Capfileを編集する

Capistranoを動作させるにはいくつかのライブラリ(Gem)を読み込む必要がある。Capfileとは、Capistrano関連のライブラリのうちどれを読み込むか指定するためのファイルである。

Capfileを以下のように編集する。
これによりデプロイに必要な動作が記述されたファイルが入ったディレクトリを読み込む。
参考

Capfile
require "capistrano/setup"
require "capistrano/deploy"
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'

Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

production.rbを編集する

config/deployディレクトリにproduction.rbとstaging.rbが作成された。
これらのファイルはデプロイについての設定を記載するファイルである。
production.rbは本番環境の設定ファイル、staging.rbはステージング環境の設定ファイルである。
production.rb(staging.rb)には下記の内容を記述する。

  • サーバホスト名
  • AWSサーバのログインユーザ名
  • サーバロール
  • SSHの設定
  • その他サーバに紐づく設定

production.rbを以下のように変更する。
(アプリケーションのElastic IPが12.345.67.890の場合)

config/deploy/production.rb
server '12.345.67.890', user: 'ec2-user', roles: %w{app db web}

開発環境、テスト環境、ステージング環境、本番環境とは

  • 開発環境
    • ローカルで動作確認などを行う。ここで問題なければテスト環境での検証を行う。
  • テスト環境
    • 誤記やリンクのミス、不具合がないかを検証する。
  • ステージング環境
    • 本番環境の前に動作や表示に問題がないかを検証する。テスト環境は本番環境とサーバの構成が異なるのに対し、ステージング環境は本番環境とサーバの構成が同じである。
  • 本番環境
    • ステージング環境で問題なければアップロードする。

deploy.rbを編集する

configディレクトリに作成されたdeploy.rbには本番環境、ステージング環境共通の設定を記述する。
具体的には以下を記述する。

  • アプリケーション名
  • gitのリポジトリ
  • 利用するSCM(Software Configuration Management,ソフトウェア構成管理)
  • タスク
  • それぞれのタスクで実行するコマンド

deploy.rbの記述を削除し、以下のように変更する。
(ここでは例としてCapistranoのバージョンが「3.11.0」、アプリケーション名が「testapp」、Githubのuser名が「test1234」、リポジトリ名が「testapp」、rubyのバージョンが「2.5.1」、ローカルPCのEC2インスタンスのSSH鍵(pem)へのパスが「~/.ssh/xxx.pem」とする。)

config/deploy.rb
# config valid only for current version of Capistrano
# capistranoのバージョンを記載。固定のバージョンを利用し続け、バージョン変更によるトラブルを防止する
lock '3.11.0'

# Capistranoのログの表示に利用する
set :application, 'testapp'

# どのリポジトリからアプリをpullするかを指定する
set :repo_url,  'git@github.com:test1234/testapp.git'

# バージョンが変わっても共通で参照するディレクトリを指定
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

set :rbenv_type, :user
set :rbenv_ruby, '2.5.1'

# どの公開鍵を利用してデプロイするか
set :ssh_options, auth_methods: ['publickey'],
                  keys: ['~/.ssh/xxx.pem'] 

# プロセス番号を記載したファイルの場所
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }

# Unicornの設定ファイルの場所
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.rb" }
set :keep_releases, 5

# デプロイ処理が終わった後、Unicornを再起動するための記述
after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end

Capistranoのバージョン確認方法

gemfile.lockというファイルに記載されている。

DSL(Domain Specific Language)について

DSLとは、特定の処理の効率を上げるために擬似的に用意されるプログラムである。
例えば、set :name, 'value'という記述がある。このとき、fetch nameとすることで'value'を取り出すことができる。setした値はdeploy.rbやproduction.rbでも取り出すことができる。
また、task :xx do〜endという記述がある。これはCapfileでrequireしたものに加えてタスクを追加している。ここで記述したものはcap deploy時に実行される。

Capistranoによる自動デプロイ後のディレクトリ構成

Capistranoによる自動デプロイが実行されると、本番環境のディレクトリ構成が変化する。Capistranoによるアプリケーションのバックアップなど、複数のディレクトリが作成される。
例えば以下のようなディレクトリが作成される。

  • releasesディレクトリ
    • Capistranoを通じてデプロイされたアプリケーションはreleasesというディレクトリにまとめられる。ここに過去のアプリケーションが残っているため、デプロイ時に何か問題が発生した時、以前のバージョンに戻すことが可能。deploy.rbのset :keep_releasesの記述は保存しておく数を指定しており、今回は5回分のバージョンを保存しておくように設定している。
  • currentディレクトリ
    • releasesディレクトリの中で最新のものが自動的にこのディレクトリにコピーされる。つまり、このディレクトリにあるアプリケーションの内容が、現在デプロイされているアプリケーションの内容ということになる。
  • sharedディレクトリ
    • バージョンが変わっても共通で参照されるディレクトリが格納される。具体的には、log,public,tmp,vendorディレクトリが格納される。

unicorn.rbを編集する

Capistranoの導入によって、本番環境のディレクトリ構成が変わるのでそれに伴いunicorn.rbの記述も変更する。

unicorn.rbを以下のように変更する。

config/unicorn.rb
#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
#変更:階層を一個深くする
app_path = File.expand_path('../../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
#変更:currentを指定
working_directory "#{app_path}/current"

#Unicornの起動に必要なファイルの設置場所を指定
#変更:sharedディレクトリ追記
pid "#{app_path}/shared/tmp/pids/unicorn.pid"

#ポート番号を指定
#変更:sharedディレクトリ追記
listen "#{app_path}/shared/tmp/sockets/unicorn.sock"

#エラーのログを記録するファイルを指定
#変更:sharedディレクトリ追記
stderr_path "#{app_path}/shared/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
#変更:sharedディレクトリ追記
stdout_path "#{app_path}/shared/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

Nginxの設定ファイル(rails.conf)を変更する

ディレクトリ構成が変わったのでrails.confも以下のように変更する。
(アプリケーション名が「testapp」、Elastic IPが「12.345.67.890」の場合を例として記載する)

rails.conf
upstream app_server {
  # sharedの中を参照するよう変更
  server unix:/var/www/testapp/shared/tmp/sockets/unicorn.sock;
}

server {
  listen 80;
  server_name 12.345.67.890;

# クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

  # currentの中を参照するよう変更
  root /var/www/testapp/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    # currentの中を参照するよう変更
    root   /var/www/testapp/current/public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}

Nginxの設定を変更したらEC2インスタンスにSSH接続し、
下記のコマンドを実行して再読み込み・再起動を行う。

sudo service nginx reload
sudo service nginx restart

MySQLを再起動する

MySQLが立ち上がっていないとデプロイできないので、
下記のコマンドを実行し念のためMySQLも再起動する。

sudo service mysqld restart

unicorn masterのプロセスをkillする

自動デプロイを実行する前にunicorn masterのプロセスをkillしておく。
まずは下記のコマンドを実行しunicorn masterのプロセスIDを確認する。

ps aux | grep unicorn

下記のコマンドで確認したプロセスをkillする。
(今回はunicorn masterのプロセスIDが17877だったとする)

kill 17877

ローカルでの修正内容をmasterにプッシュする

今回編集したファイルを全てmasterブランチにプッシュしておく。

自動デプロイを実行する

ローカル環境で下記のコマンドを実行し自動デプロイを行う。
エラーがでなければ成功。

bundle exec cap production deploy

エラーが出た時に確認すること

  • もう一度実行する
  • 記述ミスがないか
  • 手順を飛ばしていないか

ブラウザで確認する

ブラウザのURL欄にElastic IPを入力するとアプリケーションにアクセスできる(:3000をつける必要はない)。

エラーが出た時に確認すること

  • 開発環境でエラーが出ていないか
  • /var/www/testapp/current/log/unicorn.stderr.logでエラーがないか(リポジトリ名が「testapp」の場合)
  • プッシュやプルを忘れていないか
  • MySQLやNginxの再起動を行ってみる
  • EC2インスタンスの再起動を行ってみる

関連記事

AWSを使ってアプリケーションを公開する手順(1)AWSアカウントの作成
AWSを使ってアプリケーションを公開する手順(2)EC2インスタンスの作成
AWSを使ってアプリケーションを公開する方法(3)EC2インスタンスの環境構築
AWSを使ってアプリケーションを公開する手順(4)データベースの作成
AWSを使ってアプリケーションを公開する手順(5)アプリケーションを公開する
AWSを使ってアプリケーションを公開する手順(6)Nginxを導入する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CodePipelineで誰でもお手軽自動デプロイ(静的webページ編)

今回作るやつ概要

GitHub × CodePipelineで自動デプロイ :rocket:

◯おすすめな人

■ フロント系の方、web制作系の方
■ SPA公開したい方
■ CI/CDで挫折した方

準備しておくもの

■ GitHubアカウント
■ AWSアカウント
■ 静的webホスティングの設定をしたS3バケット

下記が個人的にわかりやすかったです!
【参考】AWS S3で静的Webページをホスティングする
【参考】独自ドメインを使ってAmazon S3で静的Webサイトをホストする

CodePipeline構築

CodePipelineを開きパイプラインの作成を選択。

【Step1】パイプライン設定を選択する

■パイプライン名
▫️わかりやすいものならなんでもOK
■サービスロール
▫️新しいサービスロール
■ロール名
▫️自動で入る

【Step2】ソースステージを追加する

■ソースプロバイダー
▫️GitHub
■検出オプションを変更する
▫️GitHubウェブフック(推奨)

GitHubに接続するを選択


■GitHubへSign inします。

■接続完了!!

■リポジトリ
▫️自動デプロイの対象としたいリポジトリを選択

■ブランチ
▫️対象のブランチを選択(masterでpushしたら発火的な)

【Step3】ビルドステージを追加する

今回は使用しないのでビルドステージをスキップを選択

【Step4】デプロイステージを追加する

■デプロイプロバイダー
▫️Amazon S3
■リージョン&バケット
▫️静的webホスティングの設定をしたS3バケット
■デプロイする前にファイルを抽出する
▫️チェック

【Step5】レビュー

■設定内容の確認をして下さい!

完成!!

デプロイステージに設定したバケットを見てみると、ちゃんと反映されてる!

もちろん対象のブランチでpushしたら変更が反映されます!

感想

静的webということもありましたが意外と簡単に実装できたと思います!
今回は説明しませんでしたがCloudFrontを前段に置くことで独自ドメインを使用することも出来ます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのArm版UbuntuのUnixBenchマーク

2020年12月末まで無料で使えるとのことでUbuntuの64bitArm版を試してみました。

t4g.micro 2CPU 1GBメモリ

UnixBenchマーク

sudo apt update ;
sudo apt upgrade -y ;
git clone https://github.com/kdlucas/byte-unixbench.git ;
cd byte-unixbench/ ;
cd UnixBench/ ;
sudo apt install -y make gcc ;
make ;
./Run  ;

結果

========================================================================
   BYTE UNIX Benchmarks (Version 5.1.3)

   System: ip-172-31-35-99: GNU/Linux
   OS: GNU/Linux -- 5.4.0-1024-aws -- #24-Ubuntu SMP Sat Sep 5 06:17:48 UTC 2020
   Machine: aarch64 (aarch64)
   Language: en_US.utf8 (charmap="UTF-8", collate="UTF-8")
   08:43:21 up 18 min,  1 user,  load average: 0.71, 0.65, 0.36; runlevel 2020-09-17

------------------------------------------------------------------------
Benchmark Run: Thu Sep 17 2020 08:43:21 - 09:12:43
2 CPUs in system; running 1 parallel copy of tests

Dhrystone 2 using register variables        9370962.1 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     1387.4 MWIPS (15.7 s, 7 samples)
Execl Throughput                               1205.6 lps   (30.0 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks        207354.8 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks           61433.3 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks        537176.1 KBps  (30.0 s, 2 samples)
Pipe Throughput                              471412.4 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                  14210.5 lps   (10.1 s, 7 samples)
Process Creation                               1159.0 lps   (30.1 s, 2 samples)
Shell Scripts (1 concurrent)                   1372.5 lpm   (60.1 s, 2 samples)
Shell Scripts (8 concurrent)                    226.1 lpm   (60.1 s, 2 samples)
System Call Overhead                         593986.9 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0    9370962.1    803.0
Double-Precision Whetstone                       55.0       1387.4    252.3
Execl Throughput                                 43.0       1205.6    280.4
File Copy 1024 bufsize 2000 maxblocks          3960.0     207354.8    523.6
File Copy 256 bufsize 500 maxblocks            1655.0      61433.3    371.2
File Copy 4096 bufsize 8000 maxblocks          5800.0     537176.1    926.2
Pipe Throughput                               12440.0     471412.4    378.9
Pipe-based Context Switching                   4000.0      14210.5     35.5
Process Creation                                126.0       1159.0     92.0
Shell Scripts (1 concurrent)                     42.4       1372.5    323.7
Shell Scripts (8 concurrent)                      6.0        226.1    376.9
System Call Overhead                          15000.0     593986.9    396.0
                                                                   ========
System Benchmarks Index Score                                         303.5

------------------------------------------------------------------------
Benchmark Run: Thu Sep 17 2020 09:12:43 - 09:42:10
2 CPUs in system; running 2 parallel copies of tests

Dhrystone 2 using register variables        8906679.4 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     1403.2 MWIPS (10.2 s, 7 samples)
Execl Throughput                               1039.1 lps   (29.9 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks        110651.6 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks           30134.5 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks        296691.2 KBps  (30.0 s, 2 samples)
Pipe Throughput                              482502.3 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                  68742.0 lps   (10.0 s, 7 samples)
Process Creation                               1821.0 lps   (30.1 s, 2 samples)
Shell Scripts (1 concurrent)                   1610.9 lpm   (60.1 s, 2 samples)
Shell Scripts (8 concurrent)                    224.8 lpm   (60.3 s, 2 samples)
System Call Overhead                         484504.3 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0    8906679.4    763.2
Double-Precision Whetstone                       55.0       1403.2    255.1
Execl Throughput                                 43.0       1039.1    241.6
File Copy 1024 bufsize 2000 maxblocks          3960.0     110651.6    279.4
File Copy 256 bufsize 500 maxblocks            1655.0      30134.5    182.1
File Copy 4096 bufsize 8000 maxblocks          5800.0     296691.2    511.5
Pipe Throughput                               12440.0     482502.3    387.9
Pipe-based Context Switching                   4000.0      68742.0    171.9
Process Creation                                126.0       1821.0    144.5
Shell Scripts (1 concurrent)                     42.4       1610.9    379.9
Shell Scripts (8 concurrent)                      6.0        224.8    374.7
System Call Overhead                          15000.0     484504.3    323.0
                                                                   ========
System Benchmarks Index Score                                         300.5

参考リンク

新しい EC2 T4g インスタンス – AWS Graviton2 によるバースト可能なパフォーマンス – 無料で利用可能 | Amazon Web Services ブログ
https://aws.amazon.com/jp/blogs/news/new-t4g-instances-burstable-performance-powered-by-aws-graviton2/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon GameLiftに関する基礎知識

概要

Amazon GameLiftについて調べる機会があったので、全体像を出来るだけ分かりやすいようにまとめてみた。

参考にさせて頂いたサイト

Amazon GameLift の仕組み
https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/gamelift-howitworks.html#gamelift-howitworks-placing
Amazon GameLift – ゲームサーバー/クライアントのやり取り
https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/gamelift-sdk-server-api-interaction-vsd.html
AWS再入門ブログリレー Amazon GameLift 編
https://dev.classmethod.jp/articles/re-introduction-2020-amazon-gamelift/

GameLiftとは?

Amazon GameLift は、マルチプレイヤーゲーム用のクラウドサーバーをデプロイ、運用、スケーリングする、専用のゲームサーバーホスティングソリューションです。(公式サイトより転載)

GameLiftは簡単に言うと、マルチプレイに関する色々なことをいい感じに管理/運用してくれるすごいサービス。
主にマッチング、プレイヤー人数に合わせたサーバーのスケーリング、ログの収集などをやってくれる。
FlexMatchの仕組みを使えば、様々な条件を元に最適なマッチングをすることが可能で、待機時間により条件を段階的に緩和することも出来る。
例1:最初は遅延が少ないリージョンで対戦相手を探し、しばらく見つからなかった場合は遅延の上限を緩和して探す。
例2:最初はプレイヤーレベルが近い人を探し、しばらく見つからなかった場合はプレイヤーレベルの差の上限を緩和して探す。

全体構成

ざっくりとしたイメージは以下の画像の通り。
gamelift1.png
クライアントからGameLiftにリクエストを送るフローに関しては、直接ClientSDKでGameLiftServiceに問い合わせる方法もあるが、今回の画像はAPI GatewayやLambdaを介しHttpリクエストで送る方法になっている。
このフローの方が、クライアントに直接リクエストの実装をするよりも安全で管理がしやすくなる。

専用用語

画像の構成を理解する上で最低限必要な単語をまとめてみた。

・EC2

Amazon Elastic Compute Cloudの略
AWS上にスケーリング可能な仮想サーバーを構築出来るサービスのこと

・フリート

EC2インスタンス群形式のホスティングリソース
必要に応じて、インスタンス数を調節してくれる
gamelift2.png

・エイリアス

クライアントとフリートの中継地点
エイリアスに紐づけているフリートを変更することで、クライアントを変更せずに接続先のフリートを変更することが出来る
https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/aliases-creating.html

・キュー

新しいゲームセッションを作る場合に、複数のフリートから最適なものを選ぶ為の定義
リクエスト時にプレイヤーのpingを渡すと、最適なフリート(リージョン)を選んでくれる
ping情報が無い場合、キューに登録されているリストの順番で上から選ぶ
https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/queues-design.html

・マッチメイキングコンフィグレーション

マッチングする上での様々な設定をする
https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/match-configuration.html

最低限必要な設定は以下の3つ

・ルールセット

GameLiftのダッシュボード->マッチメーキングルールセットから見れる
最大人数など、マッチングに関するルールが色々設定してある

・ゲームセッションキュー

紐付けるキュー

・リクエストのタイムアウト

タイムアウトの時間
それ以外にもいくつか設定出来る項目がある

・プレイヤーの承諾(no-acceptance-required)

マッチングに参加する場合に、承諾を要求するかどうか

・プレイヤースロットの予約(additional-player-count)

・API Gateway

クライアントからhttpリクエストを受け取る為のエンドポイント
Lambda(ラムダ)と紐付いていて、リクエストを受け取るとラムダの関数を実行してくれる

・Lambda(ラムダ)

GameLiftを操作する為の関数
APIGatewayなどをトリガーにして実行することが出来る
クライアントとGameLiftの繋ぎの役目で、クライアントに直接リクエストなどを実装するより安全で管理がしやすくなる

まとめ

今回は全体構成のざっくりしたイメージと、最低限の単語にだけ触れてみた。
AWSは公式ドキュメントや利用者の記事が多く調べやすい反面、出来ることが膨大なので、全てを理解するのは大変そうだと感じた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sequel ProでSSH Over Session Managerを使った踏み台サーバーのDBに接続する

結論

Sequel ProではユーザーのPATHを参照してくれないようなので、
brewなどでaws cliをインストールした場合、ssh_configでPATHを明示的に追加する必要があります

ssh config を準備

Host AWSEC2
  HostName i-x0x0x0xx0x0x0
  User ec2-user
  IdentityFile ~/.ssh/id_ed25519
  ProxyCommand sh -c "PATH=$PATH:/usr/local/bin/ && aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

Sequel Proに設定

SSHホスト名の欄にssh configのHostに設定した名前を入力します
SSHユーザ、SSH鍵などはssh configで設定してあるので、空欄のままで大丈夫です
image.png

MySQLへの接続情報はshellでsshしたあとに叩くmysqlコマンドを確認して入力します

mysql -h <MySQLホスト> -u<ユーザ名> -p<パスワード> <データベース>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Fargate+AWS FireLensを試す

What's?

少し前に、AWS FireLensについて調べてみたのですが、今度は使ってみようかな、ということで。

AWS FireLensってなんだ?

お題

こんなお題で試してみます。

  • AWS Fargateクラスタを構築する
  • タスクには、nginxをリバースプロキシとして、Pythonで作ったアプリケーションを背後に配置
    • ログドライバーとして、awsfirelensを使用する
  • サイドカーとして、AWS for Fluent Bitを配置して、nginxとアプリケーションのコンテナログを収集
  • 収集したコンテナログおよび、AWS for Fluent Bit自身のログは、Amazon CloudWatch Logsに送る
  • 環境は、Terraformで構築する

環境

今回の環境は、こちらです。

$ terraform version
Terraform v0.13.3
+ provider registry.terraform.io/hashicorp/aws v3.6.0

AWSのクレデンシャルは、環境変数で設定するものとします。

export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=ap-northeast-1

Amazon ECRを作成する

最初に、自前で作成するコンテナイメージを格納するための、Amazon ECRリポジトリを作成しましょう。

nginx用と、アプリケーション用の2つを用意します。

main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    aws = "3.6.0"
  }
}

provider "aws" {
}

resource "aws_ecr_repository" "nginx" {
  name = "nginx"
}

resource "aws_ecr_repository" "app" {
  name = "app"
}

作成。

$ terraform apply

Dockerイメージを作成して、Amazon ECRにPushする

続いて、Dockerイメージを作成します。

最初は、nginx。

リバースプロキシとして構築するので、そのように設定ファイルを構成。

default.conf

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    location / {
        proxy_pass   http://localhost:8000;
    }
}

これを含めるようにDockerfileを定義して

Dockerfile

FROM nginx:1.19.2

COPY default.conf /etc/nginx/conf.d/default.conf

ビルド。

$ docker image build -t charon/nginx:latest .

続いて、アプリケーション側。Flask-RESTfulを使って作成することにしました。

ソースコードは、こちら。

index.py

from datetime import datetime
import logging

from flask import Flask
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

app.logger.setLevel(logging.INFO)

class HelloWorld(Resource):
    def get(self):
        app.logger.info('access => Hello World')
        return {'message': 'Hello Flask!!', 'time': f'{datetime.now()}'}

api.add_resource(HelloWorld, '/')

if __name__ == '__main__':
    app.run(debug = True)

Dockerfileを用意します。WSGIサーバーとしては、Gunicornを使うことにしました。

Dockerfile

FROM python:3.8.5-slim-buster

COPY index.py index.py

RUN pip install flask-restful==0.3.8 gunicorn==20.0.4

EXPOSE 8000

ENTRYPOINT ["gunicorn", "index:app"]
CMD ["--threads", "10"]

ビルド。

$ docker image build -t charon/app:latest .

あとは、Amazon ECRにログインして

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com

作成したイメージに、Amazon ECR用のタグをつけてPushします。

$ docker image tag charon/nginx:latest [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
$ docker image push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
$ docker image tag charon/app:latest [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest
$ docker image push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest

AWS Fargateクラスタを構築する

それでは、AWS Fargateクラスタを構築しましょう。

定義はTerraformで書いていきます。VPCやALB、セキュリティグループ等も作成しますが、そちらのコードはまとめて最後に載せます。

main.tf

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets = module.vpc.private_subnets
  app_with_nginx_service_security_groups = [
  module.app_with_nginx_service_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

IAMロールの作成。次の2つが必要です。

Amazon ECSに対するAssume Role。

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

タスク実行用のIAMロール。

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

タスク用のIAMロール。

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name   = "MyEcsTaskPolicy"
  policy = data.aws_iam_policy_document.ecs_task_role_policy_document.json
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_role.name
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}

タスクに割り当てる権限として、今回は以下が必要になります。サイドカーとして配置する、AWS for Fluent BitコンテナがAmazon CloudWatch Logsにログを送信するに権限が必要だからです。

Fluent Bit Plugin for CloudWatch Logs / Permissions

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

これが設定できていないと、Fluent BitコンテナがAmazon CloudWatch Logsにログが送れず、こんな感じのエラーになります。

2020-09-17T07:47:10.566000+00:00 firelens/log_router/a02c7e22-4270-4587-8a7f-c98339246a5c time="2020-09-17T07:47:10Z" level=error msg="NoCredentialProviders: no valid providers in chain\ncaused by: EnvAccessKeyNotFound: failed to find credentials in the environment.\nSharedCredsLoad: failed to load profile, .\nEC2RoleRequestError: no EC2 instance role found\ncaused by: RequestError: send request failed\ncaused by: Get http://169.254.169.254/latest/meta-data/iam/security-credentials/: dial tcp 169.254.169.254:80: connect: invalid argument"

どのような権限が必要になるかは、ログの送信設定によって変わるので、利用するプラグインやリソースのドキュメントを見て設定してください。

カスタムログルーティング / 必要な IAM アクセス許可

Amazon CloudWatch Logsのロググループの作成。今回は、AWS FireLensログドライバーによって収集して送信するログも、AWS for Fluent Bit自身のログも、すべてAmazon CloudWatch Logsに送ります。

前もって作成しておきましょう。

resource "aws_cloudwatch_log_group" "nginx_container_log_group" {
  name = "/fargate/app/nginx"
}

resource "aws_cloudwatch_log_group" "app_container_log_group" {
  name = "/fargate/app/app"
}

resource "aws_cloudwatch_log_group" "firelens_container_log_group" {
  name = "/fargate/app/firelens"
}

AWS Fargetクラスタ定義。

resource "aws_ecs_cluster" "app_with_nginx" {
  name = "app-with-nginx-cluster"
}

resource "aws_ecs_task_definition" "app_with_nginx" {
  family                   = "app-with-nginx-task-definition"
  cpu                      = "1024"
  memory                   = "2048"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }
  ]
  JSON
}

resource "aws_ecs_service" "app_with_nginx" {
  name             = "app-with-nginx-service"
  cluster          = aws_ecs_cluster.app_with_nginx.arn
  task_definition  = aws_ecs_task_definition.app_with_nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  network_configuration {
    assign_public_ip = false
    security_groups  = local.app_with_nginx_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "nginx"
    container_port   = 80
  }
}

ポイントは、タスク定義に先程作成したタスク実行用のIAMロール、タスク用のIAMロールを割り当てているところと

resource "aws_ecs_task_definition" "app_with_nginx" {
  family                   = "app-with-nginx-task-definition"
  cpu                      = "1024"
  memory                   = "2048"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

コンテナ定義ですね。

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }
  ]
  JSON

AWS for Fluent Bitのコンテナは、サイドカーとして仕込みます。

    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }

AWS内で使うので、ドキュメントにしたがって、AWS for Fluent BitのイメージはAmazon ECRから取得しています。

AWS for Fluent Bitイメージの使用

typeはFluent Bitであることを示し、

FirelensConfiguration

      "firelensConfiguration": {
        "type": "fluentbit"
      },

AWS for Fluent Bitコンテナ自身のログは、Amazon CloudWatch Logsにawslogsログドライバーを使って構成します。

      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }

他の2つのコンテナは、logDriverawsfirelensに設定します。

    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },

optionsで指定した内容は、Fluent Bit(もしくはFluentd)の設定ファイルに変換されます。

FireLens構成を使用するタスク定義の作成

      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }

今回はcloudwatchを指定しているので、Fluent Bit Plugin for CloudWatch Logs向けの設定が出力されることになります。

イメージ的には、こんな感じでしょうか?

[OUTPUT]
    Name   cloudwatch
    Match  nginx-firelens*
    region ap-northeast-1
    log_group_name /fargate/app/nginx
    log_stream_prefix nginx-
    auto_create_group false

今度、ちゃんと確認してみましょう…。

ちなみに、Amazon CloudWatch Logsにログを転送する例は、ドキュメントにサンプルがあったりします。

CloudWatch Logs へのログの転送

では、AWS Fargeteを構築します。

$ terraform apply

確認

構築が終わったら、動作確認してみます。

まずは、AWS Fargate自体の動作確認。

$ curl -i [ALBのDNS名]
HTTP/1.1 200 OK
Date: Thu, 17 Sep 2020 08:57:01 GMT
Content-Type: application/json
Content-Length: 67
Connection: keep-alive
Server: nginx/1.19.2

{"message": "Hello Flask!!", "time": "2020-09-17 08:57:01.008154"}

OKです。

nginxのログを見てみます。

$ aws logs tail --follow /fargate/app/nginx
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"10-listen-on-ipv6-by-default.sh: error: IPv6 listen already enabled","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Configuration complete; ready for start up","source":"stdout"}

〜省略〜

ログストリーム名は、prefix+タグ+コンテナIDみたいですね。

Fluent Bit Plugin for CloudWatch Logs / Plugin Options

nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3

アプリケーションのログ。

$ aws logs tail --follow /fargate/app/app
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [1] [INFO] Starting gunicorn 20.0.4","source":"stderr"}
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [1] [INFO] Listening at: http://127.0.0.1:8000 (1)","source":"stderr"}
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [1] [INFO] Using worker: threads","source":"stderr"}
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [9] [INFO] Booting worker with pid: 9","source":"stderr"}
2020-09-17T08:56:21+00:00 app-app-firelens-12fe7680-7b28-4e4f-a6c8-9d946bb0d49e {"container_id":"12fe7680-7b28-4e4f-a6c8-9d946bb0d49e-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/12fe7680-7b28-4e4f-a6c8-9d946bb0d49e","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:21 +0000] [1] [INFO] Starting gunicorn 20.0.4","source":"stderr"}

〜省略〜

AWS for Fluent Bitのログ。こちらは、awslogsロギングドライバーの利用になります。

$ aws logs tail --follow /fargate/app/firelens

起動時のログ。

2020-09-17T08:56:19.100000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 AWS for Fluent Bit Container Image Version 2.7.0
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 Fluent Bit v1.5.6
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * Copyright (C) 2019-2020 The Fluent Bit Authors
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * Copyright (C) 2015-2018 Treasure Data
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * https://fluentbit.io
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_group = '/fargate/app/nginx'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_stream_prefix = 'nginx-'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_stream_name = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter region = 'ap-northeast-1'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_key = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter role_arn = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter new_log_group_tags = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_retention_days = '0'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter sts_endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter credentials_endpoint = "
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_format = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_group = '/fargate/app/app'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_stream_prefix = 'app-'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_stream_name = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter region = 'ap-northeast-1'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_key = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter role_arn = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter new_log_group_tags = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_retention_days = '0'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter sts_endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter credentials_endpoint = "
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_format = ''"

確かに、各コンテナのlogConfigurationの内容がプラグインの設定として渡されているようですね。

msg="[cloudwatch 0] plugin parameter log_group = '/fargate/app/nginx'"
msg="[cloudwatch 0] plugin parameter log_stream_prefix = 'nginx-'"
msg="[cloudwatch 0] plugin parameter log_stream_name = ''"
msg="[cloudwatch 0] plugin parameter region = 'ap-northeast-1'"
msg="[cloudwatch 0] plugin parameter log_key = ''"
msg="[cloudwatch 0] plugin parameter role_arn = ''"
msg="[cloudwatch 0] plugin parameter new_log_group_tags = ''"
msg="[cloudwatch 0] plugin parameter log_retention_days = '0'"
msg="[cloudwatch 0] plugin parameter endpoint = ''"
msg="[cloudwatch 0] plugin parameter sts_endpoint = ''"
msg="[cloudwatch 0] plugin parameter credentials_endpoint = "
msg="[cloudwatch 0] plugin parameter log_format = ''"
msg="[cloudwatch 1] plugin parameter log_group = '/fargate/app/app'"
msg="[cloudwatch 1] plugin parameter log_stream_prefix = 'app-'"
msg="[cloudwatch 1] plugin parameter log_stream_name = ''"
msg="[cloudwatch 1] plugin parameter region = 'ap-northeast-1'"
msg="[cloudwatch 1] plugin parameter log_key = ''"
msg="[cloudwatch 1] plugin parameter role_arn = ''"
msg="[cloudwatch 1] plugin parameter new_log_group_tags = ''"
msg="[cloudwatch 1] plugin parameter log_retention_days = '0'"
msg="[cloudwatch 1] plugin parameter endpoint = ''"
msg="[cloudwatch 1] plugin parameter sts_endpoint = ''"
msg="[cloudwatch 1] plugin parameter credentials_endpoint = "
msg="[cloudwatch 1] plugin parameter log_format = ''"

動作確認はできたので、OKとしましょう!

VPC〜ALBまで(〜AWS Fargateも)

最後に、省略していたVPCからALBまでの定義を含めた、全体のTerraform定義を載せておきます。

main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    aws = "3.6.0"
  }
}

provider "aws" {
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.51.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  enable_dns_hostnames = true
  enable_dns_support   = true

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.10.0/24", "10.0.20.0/24"]
  private_subnets = ["10.0.30.0/24", "10.0.40.0/24"]

  map_public_ip_on_launch = true

  enable_nat_gateway     = true
  single_nat_gateway     = false
  one_nat_gateway_per_az = true
}

module "load_balancer_sg" {
  source  = "terraform-aws-modules/security-group/aws//modules/http-80"
  version = "3.16.0"

  name   = "load-balancer-sg"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
}

module "app_with_nginx_service_sg" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "3.16.0"

  name   = "app-with-nginx-service-sg"
  vpc_id = module.vpc.vpc_id

  ingress_with_cidr_blocks = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      description = "app-with-nginx-service inbound ports"
      cidr_blocks = "10.0.10.0/24"
    },
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      description = "app-with-nginx-service inbound ports"
      cidr_blocks = "10.0.20.0/24"
    }
  ]

  egress_with_cidr_blocks = [
    {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      description = "app-with-nginx-service outbound ports"
      cidr_blocks = "0.0.0.0/0"
    }
  ]
}

module "load_balancer" {
  source  = "terraform-aws-modules/alb/aws"
  version = "5.9.0"

  name = "app-with-nginx"

  vpc_id             = module.vpc.vpc_id
  load_balancer_type = "application"
  internal           = false

  subnets         = module.vpc.public_subnets
  security_groups = [module.load_balancer_sg.this_security_group_id]

  target_groups = [
    {
      backend_protocol = "HTTP"
      backend_port     = 80
      target_type      = "ip"

      health_check = {
        interval = 20
      }
    }
  ]

  http_tcp_listeners = [
    {
      port     = 80
      protocol = "HTTP"
    }
  ]
}

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets = module.vpc.private_subnets
  app_with_nginx_service_security_groups = [
  module.app_with_nginx_service_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name   = "MyEcsTaskPolicy"
  policy = data.aws_iam_policy_document.ecs_task_role_policy_document.json
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_role.name
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}

resource "aws_cloudwatch_log_group" "nginx_container_log_group" {
  name = "/fargate/app/nginx"
}

resource "aws_cloudwatch_log_group" "app_container_log_group" {
  name = "/fargate/app/app"
}

resource "aws_cloudwatch_log_group" "firelens_container_log_group" {
  name = "/fargate/app/firelens"
}

resource "aws_ecs_cluster" "app_with_nginx" {
  name = "app-with-nginx-cluster"
}

resource "aws_ecs_task_definition" "app_with_nginx" {
  family                   = "app-with-nginx-task-definition"
  cpu                      = "1024"
  memory                   = "2048"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }
  ]
  JSON
}

resource "aws_ecs_service" "app_with_nginx" {
  name             = "app-with-nginx-service"
  cluster          = aws_ecs_cluster.app_with_nginx.arn
  task_definition  = aws_ecs_task_definition.app_with_nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  network_configuration {
    assign_public_ip = false
    security_groups  = local.app_with_nginx_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "nginx"
    container_port   = 80
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Fargate+AWS FireLens(Fluent Bit Plugin for CloudWatch Logs)を試す

What's?

少し前に、AWS FireLensについて調べてみたのですが、今度は使ってみようかな、ということで。

AWS FireLensってなんだ?

お題

こんなお題で試してみます。

  • AWS Fargateクラスタを構築する
  • タスクには、nginxをリバースプロキシとして、Pythonで作ったアプリケーションを背後に配置
    • ログドライバーとして、awsfirelensを使用する
  • サイドカーとして、AWS for Fluent Bitを配置して、nginxとアプリケーションのコンテナログを受け取る
  • 受け取った他のコンテナログおよび、AWS for Fluent Bit自身のログは、Amazon CloudWatch Logsに送る
  • 環境は、Terraformで構築する

環境

今回の環境は、こちらです。

$ terraform version
Terraform v0.13.3
+ provider registry.terraform.io/hashicorp/aws v3.6.0

AWSのクレデンシャルは、環境変数で設定するものとします。

export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=ap-northeast-1

Amazon ECRを作成する

最初に、自前で作成するコンテナイメージを格納するための、Amazon ECRリポジトリを作成しましょう。

nginx用と、アプリケーション用の2つを用意します。

main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    aws = "3.6.0"
  }
}

provider "aws" {
}

resource "aws_ecr_repository" "nginx" {
  name = "nginx"
}

resource "aws_ecr_repository" "app" {
  name = "app"
}

作成。

$ terraform apply

Dockerイメージを作成して、Amazon ECRにPushする

続いて、Dockerイメージを作成します。

最初は、nginx。

リバースプロキシとして構築するので、そのように設定ファイルを構成。

default.conf

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    location / {
        proxy_pass   http://localhost:8000;
    }
}

これを含めるようにDockerfileを定義して

Dockerfile

FROM nginx:1.19.2

COPY default.conf /etc/nginx/conf.d/default.conf

ビルド。

$ docker image build -t charon/nginx:latest .

続いて、アプリケーション側。Flask-RESTfulを使って作成することにしました。

ソースコードは、こちら。

index.py

from datetime import datetime
import logging

from flask import Flask
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

app.logger.setLevel(logging.INFO)

class HelloWorld(Resource):
    def get(self):
        app.logger.info('access => Hello World')
        return {'message': 'Hello Flask!!', 'time': f'{datetime.now()}'}

api.add_resource(HelloWorld, '/')

if __name__ == '__main__':
    app.run(debug = True)

Dockerfileを用意します。WSGIサーバーとしては、Gunicornを使うことにしました。

Dockerfile

FROM python:3.8.5-slim-buster

COPY index.py index.py

RUN pip install flask-restful==0.3.8 gunicorn==20.0.4

EXPOSE 8000

ENTRYPOINT ["gunicorn", "index:app"]
CMD ["--threads", "10"]

ビルド。

$ docker image build -t charon/app:latest .

あとは、Amazon ECRにログインして

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com

作成したイメージに、Amazon ECR用のタグをつけてPushします。

$ docker image tag charon/nginx:latest [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
$ docker image push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
$ docker image tag charon/app:latest [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest
$ docker image push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest

AWS Fargateクラスタを構築する

それでは、AWS Fargateクラスタを構築しましょう。

定義はTerraformで書いていきます。VPCやALB、セキュリティグループ等も作成しますが、そちらのコードはまとめて最後に載せます。

main.tf

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets = module.vpc.private_subnets
  app_with_nginx_service_security_groups = [
  module.app_with_nginx_service_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

IAMロールの作成。次の2つが必要です。

Amazon ECSに対するAssume Role。

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

タスク実行用のIAMロール。

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

タスク用のIAMロール。

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name   = "MyEcsTaskPolicy"
  policy = data.aws_iam_policy_document.ecs_task_role_policy_document.json
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_role.name
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}

タスクに割り当てる権限として、今回は以下が必要になります。サイドカーとして配置する、AWS for Fluent BitコンテナがAmazon CloudWatch Logsにログを送信するに権限が必要だからです。

Fluent Bit Plugin for CloudWatch Logs / Permissions

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

これが設定できていないと、Fluent BitコンテナがAmazon CloudWatch Logsにログが送れず、こんな感じのエラーになります。

2020-09-17T07:47:10.566000+00:00 firelens/log_router/a02c7e22-4270-4587-8a7f-c98339246a5c time="2020-09-17T07:47:10Z" level=error msg="NoCredentialProviders: no valid providers in chain\ncaused by: EnvAccessKeyNotFound: failed to find credentials in the environment.\nSharedCredsLoad: failed to load profile, .\nEC2RoleRequestError: no EC2 instance role found\ncaused by: RequestError: send request failed\ncaused by: Get http://169.254.169.254/latest/meta-data/iam/security-credentials/: dial tcp 169.254.169.254:80: connect: invalid argument"

どのような権限が必要になるかは、ログの送信設定によって変わるので、利用するプラグインやリソースのドキュメントを見て設定してください。

カスタムログルーティング / 必要な IAM アクセス許可

Amazon CloudWatch Logsのロググループの作成。今回は、AWS FireLensログドライバーによって収集して送信するログも、AWS for Fluent Bit自身のログも、すべてAmazon CloudWatch Logsに送ります。

前もって作成しておきましょう。

resource "aws_cloudwatch_log_group" "nginx_container_log_group" {
  name = "/fargate/app/nginx"
}

resource "aws_cloudwatch_log_group" "app_container_log_group" {
  name = "/fargate/app/app"
}

resource "aws_cloudwatch_log_group" "firelens_container_log_group" {
  name = "/fargate/app/firelens"
}

AWS Fargetクラスタ定義。

resource "aws_ecs_cluster" "app_with_nginx" {
  name = "app-with-nginx-cluster"
}

resource "aws_ecs_task_definition" "app_with_nginx" {
  family                   = "app-with-nginx-task-definition"
  cpu                      = "1024"
  memory                   = "2048"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }
  ]
  JSON
}

resource "aws_ecs_service" "app_with_nginx" {
  name             = "app-with-nginx-service"
  cluster          = aws_ecs_cluster.app_with_nginx.arn
  task_definition  = aws_ecs_task_definition.app_with_nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  network_configuration {
    assign_public_ip = false
    security_groups  = local.app_with_nginx_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "nginx"
    container_port   = 80
  }
}

ポイントは、タスク定義に先程作成したタスク実行用のIAMロール、タスク用のIAMロールを割り当てているところと

resource "aws_ecs_task_definition" "app_with_nginx" {
  family                   = "app-with-nginx-task-definition"
  cpu                      = "1024"
  memory                   = "2048"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

コンテナ定義ですね。

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }
  ]
  JSON

AWS for Fluent Bitのコンテナは、サイドカーとして仕込みます。

    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }

AWS内で使うので、ドキュメントにしたがって、AWS for Fluent BitのイメージはAmazon ECRから取得しています。

AWS for Fluent Bitイメージの使用

typeはFluent Bitであることを示し、

FirelensConfiguration

      "firelensConfiguration": {
        "type": "fluentbit"
      },

AWS for Fluent Bitコンテナ自身のログは、Amazon CloudWatch Logsにawslogsログドライバーを使って構成します。

      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }

他の2つのコンテナは、logDriverawsfirelensに設定します。

    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },

optionsで指定した内容は、Fluent Bit(もしくはFluentd)の設定ファイルに変換されます。

FireLens構成を使用するタスク定義の作成

      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }

今回はcloudwatchを指定しているので、Fluent Bit Plugin for CloudWatch Logs向けの設定が出力されることになります。

イメージ的には、こんな感じでしょうか?

[OUTPUT]
    Name   cloudwatch
    Match  nginx-firelens*
    region ap-northeast-1
    log_group_name /fargate/app/nginx
    log_stream_prefix nginx-
    auto_create_group false

今度、ちゃんと確認してみましょう…。

ちなみに、Amazon CloudWatch Logsにログを転送する例は、ドキュメントにサンプルがあったりします。

CloudWatch Logs へのログの転送

では、AWS Fargeteを構築します。

$ terraform apply

確認

構築が終わったら、動作確認してみます。

まずは、AWS Fargate自体の動作確認。

$ curl -i [ALBのDNS名]
HTTP/1.1 200 OK
Date: Thu, 17 Sep 2020 08:57:01 GMT
Content-Type: application/json
Content-Length: 67
Connection: keep-alive
Server: nginx/1.19.2

{"message": "Hello Flask!!", "time": "2020-09-17 08:57:01.008154"}

OKです。

nginxのログを見てみます。

$ aws logs tail --follow /fargate/app/nginx
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"10-listen-on-ipv6-by-default.sh: error: IPv6 listen already enabled","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh","source":"stdout"}
2020-09-17T08:56:19+00:00 nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-2531612879","container_name":"nginx","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"/docker-entrypoint.sh: Configuration complete; ready for start up","source":"stdout"}

〜省略〜

ログストリーム名は、prefix+タグ+コンテナIDみたいですね。

Fluent Bit Plugin for CloudWatch Logs / Plugin Options

nginx-nginx-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3

アプリケーションのログ。

$ aws logs tail --follow /fargate/app/app
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [1] [INFO] Starting gunicorn 20.0.4","source":"stderr"}
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [1] [INFO] Listening at: http://127.0.0.1:8000 (1)","source":"stderr"}
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [1] [INFO] Using worker: threads","source":"stderr"}
2020-09-17T08:56:19+00:00 app-app-firelens-f5c7c43b-71d9-4e99-8609-28f7d4c116e3 {"container_id":"f5c7c43b-71d9-4e99-8609-28f7d4c116e3-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/f5c7c43b-71d9-4e99-8609-28f7d4c116e3","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:19 +0000] [9] [INFO] Booting worker with pid: 9","source":"stderr"}
2020-09-17T08:56:21+00:00 app-app-firelens-12fe7680-7b28-4e4f-a6c8-9d946bb0d49e {"container_id":"12fe7680-7b28-4e4f-a6c8-9d946bb0d49e-527074092","container_name":"app","ecs_cluster":"app-with-nginx-cluster","ecs_task_arn":"arn:aws:ecs:ap-northeast-1:[AWSアカウントID]:task/12fe7680-7b28-4e4f-a6c8-9d946bb0d49e","ecs_task_definition":"app-with-nginx-task-definition:5","log":"[2020-09-17 08:56:21 +0000] [1] [INFO] Starting gunicorn 20.0.4","source":"stderr"}

〜省略〜

AWS for Fluent Bitのログ。こちらは、awslogsロギングドライバーの利用になります。

$ aws logs tail --follow /fargate/app/firelens

起動時のログ。

2020-09-17T08:56:19.100000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 AWS for Fluent Bit Container Image Version 2.7.0
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 Fluent Bit v1.5.6
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * Copyright (C) 2019-2020 The Fluent Bit Authors
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * Copyright (C) 2015-2018 Treasure Data
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
2020-09-17T08:56:19.148000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 * https://fluentbit.io
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_group = '/fargate/app/nginx'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_stream_prefix = 'nginx-'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_stream_name = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter region = 'ap-northeast-1'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_key = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter role_arn = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter new_log_group_tags = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_retention_days = '0'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter sts_endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter credentials_endpoint = "
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 0] plugin parameter log_format = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_group = '/fargate/app/app'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_stream_prefix = 'app-'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_stream_name = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter region = 'ap-northeast-1'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_key = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter role_arn = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter new_log_group_tags = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_retention_days = '0'"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter sts_endpoint = ''"
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter credentials_endpoint = "
2020-09-17T08:56:19.149000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 time="2020-09-17T08:56:19Z" level=info msg="[cloudwatch 1] plugin parameter log_format = ''"
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [engine] started (pid=1)
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [storage] version=1.0.5, initializing...
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [storage] in-memory
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [storage] normal synchronization mode, checksum disabled, max_chunks_up=128
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [input:tcp:tcp.0] listening on 127.0.0.1:8877
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [input:forward:forward.1] listening on unix:///var/run/fluent.sock
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [input:forward:forward.2] listening on 127.0.0.1:24224
2020-09-17T08:56:19.150000+00:00 firelens/log_router/f5c7c43b-71d9-4e99-8609-28f7d4c116e3 [2020/09/17 08:56:19] [ info] [sp] stream processor started

確かに、各コンテナのlogConfigurationの内容がプラグインの設定として渡されているようですね。

msg="[cloudwatch 0] plugin parameter log_group = '/fargate/app/nginx'"
msg="[cloudwatch 0] plugin parameter log_stream_prefix = 'nginx-'"
msg="[cloudwatch 0] plugin parameter log_stream_name = ''"
msg="[cloudwatch 0] plugin parameter region = 'ap-northeast-1'"
msg="[cloudwatch 0] plugin parameter log_key = ''"
msg="[cloudwatch 0] plugin parameter role_arn = ''"
msg="[cloudwatch 0] plugin parameter new_log_group_tags = ''"
msg="[cloudwatch 0] plugin parameter log_retention_days = '0'"
msg="[cloudwatch 0] plugin parameter endpoint = ''"
msg="[cloudwatch 0] plugin parameter sts_endpoint = ''"
msg="[cloudwatch 0] plugin parameter credentials_endpoint = "
msg="[cloudwatch 0] plugin parameter log_format = ''"
msg="[cloudwatch 1] plugin parameter log_group = '/fargate/app/app'"
msg="[cloudwatch 1] plugin parameter log_stream_prefix = 'app-'"
msg="[cloudwatch 1] plugin parameter log_stream_name = ''"
msg="[cloudwatch 1] plugin parameter region = 'ap-northeast-1'"
msg="[cloudwatch 1] plugin parameter log_key = ''"
msg="[cloudwatch 1] plugin parameter role_arn = ''"
msg="[cloudwatch 1] plugin parameter new_log_group_tags = ''"
msg="[cloudwatch 1] plugin parameter log_retention_days = '0'"
msg="[cloudwatch 1] plugin parameter endpoint = ''"
msg="[cloudwatch 1] plugin parameter sts_endpoint = ''"
msg="[cloudwatch 1] plugin parameter credentials_endpoint = "
msg="[cloudwatch 1] plugin parameter log_format = ''"

動作確認はできたのと、ある程度動きはわかったので、今回はこれでOKとしましょう!

VPC〜ALBまで(〜AWS Fargateも)

最後に、省略していたVPCからALBまでの定義を含めた、全体のTerraform定義を載せておきます。

main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    aws = "3.6.0"
  }
}

provider "aws" {
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.51.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  enable_dns_hostnames = true
  enable_dns_support   = true

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.10.0/24", "10.0.20.0/24"]
  private_subnets = ["10.0.30.0/24", "10.0.40.0/24"]

  map_public_ip_on_launch = true

  enable_nat_gateway     = true
  single_nat_gateway     = false
  one_nat_gateway_per_az = true
}

module "load_balancer_sg" {
  source  = "terraform-aws-modules/security-group/aws//modules/http-80"
  version = "3.16.0"

  name   = "load-balancer-sg"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
}

module "app_with_nginx_service_sg" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "3.16.0"

  name   = "app-with-nginx-service-sg"
  vpc_id = module.vpc.vpc_id

  ingress_with_cidr_blocks = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      description = "app-with-nginx-service inbound ports"
      cidr_blocks = "10.0.10.0/24"
    },
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      description = "app-with-nginx-service inbound ports"
      cidr_blocks = "10.0.20.0/24"
    }
  ]

  egress_with_cidr_blocks = [
    {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      description = "app-with-nginx-service outbound ports"
      cidr_blocks = "0.0.0.0/0"
    }
  ]
}

module "load_balancer" {
  source  = "terraform-aws-modules/alb/aws"
  version = "5.9.0"

  name = "app-with-nginx"

  vpc_id             = module.vpc.vpc_id
  load_balancer_type = "application"
  internal           = false

  subnets         = module.vpc.public_subnets
  security_groups = [module.load_balancer_sg.this_security_group_id]

  target_groups = [
    {
      backend_protocol = "HTTP"
      backend_port     = 80
      target_type      = "ip"

      health_check = {
        interval = 20
      }
    }
  ]

  http_tcp_listeners = [
    {
      port     = 80
      protocol = "HTTP"
    }
  ]
}

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets = module.vpc.private_subnets
  app_with_nginx_service_security_groups = [
  module.app_with_nginx_service_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_role_policy_document" {
  statement {
    effect = "Allow"

    actions = [
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name   = "MyEcsTaskPolicy"
  policy = data.aws_iam_policy_document.ecs_task_role_policy_document.json
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_role.name
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}

resource "aws_cloudwatch_log_group" "nginx_container_log_group" {
  name = "/fargate/app/nginx"
}

resource "aws_cloudwatch_log_group" "app_container_log_group" {
  name = "/fargate/app/app"
}

resource "aws_cloudwatch_log_group" "firelens_container_log_group" {
  name = "/fargate/app/firelens"
}

resource "aws_ecs_cluster" "app_with_nginx" {
  name = "app-with-nginx-cluster"
}

resource "aws_ecs_task_definition" "app_with_nginx" {
  family                   = "app-with-nginx-task-definition"
  cpu                      = "1024"
  memory                   = "2048"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn            = aws_iam_role.ecs_task_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "nginx",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/nginx",
          "log_stream_prefix": "nginx-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "app",
      "image": "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/app:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "logConfiguration": {
        "logDriver": "awsfirelens",
        "options": {
          "Name": "cloudwatch",
          "region": "ap-northeast-1",
          "log_group_name": "/fargate/app/app",
          "log_stream_prefix": "app-",
          "auto_create_group": "false"
        }
      }
    },
    {
      "name": "log_router",
      "image": "906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest",
      "essential": true,
      "cpu": 256,
      "memory": 512,
      "firelensConfiguration": {
        "type": "fluentbit"
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/fargate/app/firelens",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "firelens"
        }
      }
    }
  ]
  JSON
}

resource "aws_ecs_service" "app_with_nginx" {
  name             = "app-with-nginx-service"
  cluster          = aws_ecs_cluster.app_with_nginx.arn
  task_definition  = aws_ecs_task_definition.app_with_nginx.arn
  desired_count    = 3
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  network_configuration {
    assign_public_ip = false
    security_groups  = local.app_with_nginx_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "nginx"
    container_port   = 80
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rake secretできない問題

secret_key_baseを取得したいのに、rake secretを実行するとエラーが出る…

rake aborted!
Errno::EACCES: Permission denied @ dir_s_mkdir - /var/www/filma/tmp/cache
/var/www/filma/config/boot.rb:4:in `require'
/var/www/filma/config/boot.rb:4:in `<top (required)>'
/var/www/filma/config/application.rb:1:in `require_relative'
/var/www/filma/config/application.rb:1:in `<top (required)>'
/var/www/filma/Rakefile:4:in `require_relative'
/var/www/filma/Rakefile:4:in `<top (required)>'
/home/ec2-user/.rbenv/versions/2.6.6/bin/bundle:23:in `load'
/home/ec2-user/.rbenv/versions/2.6.6/bin/bundle:23:in `<main>'
(See full trace by running task with --trace)

全然意味がわからないので調べてみると…

Permission denied = ”権限がない”

こちらの記事が参考になりました。

Permission deniedが出る=”権限がない状態”ということなんですね。(合ってる?)

権限を確認

ec2-user@~ アプリ名 $ ls -la

を実行すると…

drwxr-xr-x 15 root     root   4096  9月 17 06:23 .
drwxr-xr-x  3 ec2-user root     19  9月 17 06:23 ..
-rw-r--r--  1 root     root  10244  9月 17 06:23 .DS_Store
・
・
・
-rw-r--r--  1 root     root 324953  9月 17 06:23 yarn.lock

▲ほとんどrootユーザーにしか権限が与えられてない!

解決方法

ユーザー名の確認(一応)▼

ec2-user@~ アプリ名 $ whoami

ec2-user

権限を与える

ec2-user@~ アプリ名 $ sudo chown -R ec2-user .

これで解決しました!

ec2-user@~ アプリ名 $ rake secret

*********************************

▲無事に取得完了

前やったときは出なかったエラーが出るのは勘弁してほしいですが…少しずつ検索力も身についてきたかもしれないのでよし。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails6対応, 公式SDK] AWS SESを使ってRailsから送信元が独自ドメインのメールを送ってみた

AWS SESを使ってRailsから送信元が独自ドメインのメールを送ってみた - Rails 6, Credentials 仕様-

IAMユーザーを使用する方法で私の環境と同じエラーが散見され
EC2にroleをアタッチする方法などを試みていたが
公式のSDKを使用することで解決できたので書き留めます

環境

Rails: 6.0.3
Ruby: 2.7.1

事前準備

  • SES、ドメインの設定
  • SES関連の権限が付与されたIAMユーザー
  • 上記ユーザーのaccess_key_id, secret_access_keyを取得

ここまでは下記ページを参考にしました
【AWS】Amazon SES / Messaging・Route53を用いてドメインメールを送信する - Qiita

以下、参考ページからの変更点

  • Rails 6のcredentialsを使用する
  • SESのリージョンをap-northeast-1に変更
  • AWS公式のSDKを使用

    特にここが重要で私の環境では一通り設定した後の送信テストで次のエラーが

    AWS::SES::ResponseError (AWS::SES Response Error: InvalidClientTokenId - The security token included in the request is invalid.)
    
    

    以下のSDKが使用されているところを

    gem 'aws-ses', '~> 0.6'
    
    

    version 0.6.0が2014年アップデートのため

    gem 'aws-ses', '~> 0.7'
    
    

    0.7.0 - September 03, 2020を試したものの変化なし

    AWS公式のSDKがあることがわかったのでこちらを試したところ解決しました
    Ruby on Rails で SDK を使用する - AWS SDK for Ruby

    aws-sdk-rails/README.md at master · aws/aws-sdk-rails

    gem 'aws-sdk-rails', '~> 3'
    
    

credentials.yml.encにAWSにアクセスするための情報を記述

EDITOR=vim rails credentials:edit

credentials.yml.encは直接開けず、master keyで復号化する必要があるため上記コマンドで。EDITORの指定は必須、Dockerでalpineイメージを使っているなど、場合によってEDITOR=vi

aws:の部分のコメントアウトを解除しaccess_key_id, secret_access_keyを追加

aws:
  access_key_id: YOUR_KEY_ID
  secret_access_key: YOUR_ACCESS_KEY

# Used as the base secret for all MessageVerifiers in Rails, including the one prote
secret_key_base:***********************************

Action mailerの設定

config/initializers/aws.rb (新規作成)
credentialsからaccess_key_id, secret_access_keyを呼び出しています

creds = Aws::Credentials.new(Rails.application.credentials[:aws][:access_key_id], Rails.application.credentials[:aws][:secret_access_key])

Aws::Rails.add_action_mailer_delivery_method(
  :ses,
  credentials: creds,
  region: 'ap-northeast-1' #AWS SESで設定したregion 
)

config/environments/development.rb

  config.action_mailer.delivery_method = :ses

動作確認

テスト用のmailerを作ってみます

rails g mailer TestMailer test

app/mailers/test_mailer.rb

class DefaultMailer < ApplicationMailer
  default from: 'example.com <noreply@example.com>' #example.comに自分のドメインを追加

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.default_mailer.test.subject
  #
  def test
    @greeting = "Hi"

    mail to: "テストメールを受信するアドレス" #追加
  end
end

Rails c

TestMailer.test.deliver_now

これでnoreply@...からメールが送信されるはずです

お掃除

rails d mailer TestMailer

Troubleshoot

テスト送信がうまく行かない場合は以下のオプションでデバックに必要な情報が得られるかもしれません
config/environments/development.rb

config.action_mailer.raise_delivery_errors = true

インターフェイス

formからpost :send_mailで受けて

controller

def send_mail
  @user = User.new(user_params)
  DefaultMailer.test(@user).deliver_now
  redirect_to ...
end

app/mailers/default_mailer.rb

class DefaultMailer < ApplicationMailer
  default from: 'example.com <noreply@example.com>'

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.default_mailer.test.subject
  #
  def test(user)
    @user = user
    @greeting = "Hi"
    mail to: @user.email
  end
end

app/views/default_mailer/test.html.erb

<h1>Default#test</h1>

<p>
  <%= @greeting %>, <%= @user.name%> Thank you for counfirm!
</p>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RDS のコンソール表示で詰まった話

<概要>
RDSのDBエンジンの更新を実施しようと思ったのだが、
旧コンソールでは「DBエンジンのバージョン変更」が表示されず焦った。

クラスター > クラスターの変更 > DBエンジンのバージョン変更 ←これが表示されない。

サポートに問い合わせた所、IE11じゃあかんのらしい。IEEE!!!
Chrome , Firefoxでないとだめだそうな。

IE をご利用いただいており、旧 RDS コンソールが表示された状態であることを確認いたしました。
誠に恐れながら、現状 IE11 では RDS の新コンソールをご利用いただくことができず、このため新 RDS コンソールにて実装されている機能を使用することができません。
クラスターのエンジンバージョンの変更につきましても、旧コンソールでは対応しておらず、新コンソールまたは AWS CLI などを使用する必要がございます。

このため、大変恐れ入りますが、現在ご利用いただいております IE 以外の Google Chrome や Firefox などのブラウザをご利用いただくことで、新 RDS コンソールにアクセスでき、またクラスターのエンジンバージョンの変更が可能となるかご確認いただけますでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む