Linux のログイン・ログオフ履歴を Cloudwatch Logs に送信する

サマリ

Linux のログイン/ログオフの履歴(だけ)を Cloudwatch Logs に送りたかった。
rsyslog から sshd のログだけ抽出して Cloudwatch Logs Agent で送った。できた。

要件

Linux へのログイン・ログオフの履歴を Cloudwatch Logs に保存する必要があり、Cloudwatch Logs Agent をインストールして /var/log/secure を Cloudwatch Logs に送信する。
ただし、トラフィック量の都合で secure 全て送るのはよろしくないのでいい感じに絞ったものを送信したい。

SSH ログイン時に出力されるログには2種類あり、

  1. sshd プロセスが出力する rsyslog

    • /var/log/secure
  2. login プロセス出力するログ達(バイナリ形式)
    • /var/log/wtmp (ログイン成功ログ)

      • last コマンドで表示、新しいものが上で並ぶ
    • /var/log/btmp (ログイン失敗ログ)
      • lastb コマンドで表示、新しいものが上で並ぶ

送信するにはテキストである必要があるため、

  1. rsyslog からうまいこと sshd プロセスのログだけを別ファイルにする

    • rsyslog で出力されたログをCloudwatch Logs Agent で送信する
  2. last/lastbコマンドを実行し、結果をログファイルに出力するシェルスクリプトを作成する。その際、時系列降順に直す必要がある。
    • cronで定期的に実行し、ログをCloudwatch Logs Agent で送信する

の2パターン考えられるが、楽そうな前者を試す。

設定手順

環境

  • Red Hat Enterprise Linux 7.4 (HVM)
  • awscli-cwlogs 1.4.4

SSHログの抽出

適当な EC2 インスタンスを起動。

要件としては、rsyslog の secure ログ /var/log/secure| grep 'sshd'したような結果が出力できればよさそう。そのほかの secure ログはいらない。

console
$ sudo cat /var/log/secure | grep 'sshd'
略
Jan 23 19:41:46 HOSTNAME sshd[5106]: Server listening on 0.0.0.0 port 22.
Jan 23 19:41:46 HOSTNAME sshd[5106]: Server listening on :: port 22.
Jan 23 20:40:54 HOSTNAME sshd[1976]: pam_unix(sshd:session): session closed for user ec2-user
Jan 23 20:47:46 HOSTNAME sshd[4914]: Accepted publickey for ec2-user from 10.0.0.2 port 61646 ssh2: RSA SHA256:xxx
Jan 23 20:47:46 HOSTNAME sshd[4914]: pam_unix(sshd:session): session opened for user ec2-user by (uid=0)
Jan 23 20:49:12 HOSTNAME sshd[4914]: pam_unix(sshd:session): session closed for user ec2-user

rsysgにはプロパティベース フィルタというものがあり、 :property, [!]compare-operation, "value"
programname でフィルタがかけられる。これを利用すれば特定のプロセスのログだけ別ファイルに出力することが可能。
なので rsyslog の設定をしていく。secure_sshd という新しいログファイルに sshd プロセスのログだけを出力する設定を追記。

/etc/rsyslog.conf
# sshd ログを別ファイルにも出力
:programname, isequal, "sshd" /var/log/secure_sshd
console
sudo service rsyslog restart

ログイン・ログオフしてから良い感じにログが出ていること確認できた。

console
$ sudo cat /var/log/secure_sshd
Jan 24 03:08:55 HOSTNAME sshd[9308]: Accepted publickey for ec2-user from 10.0.0.3 port 60196 ssh2: RSA SHA256:xxx
Jan 24 03:08:55 HOSTNAME sshd[9308]: pam_unix(sshd:session): session opened for user ec2-user by (uid=0)
Jan 24 03:09:14 HOSTNAME sshd[9308]: pam_unix(sshd:session): session closed for user ec2-user

Cloudwatch Agent 側設定

EC2インスタンスに CloudwatchLogs 用のポリシーをアタッチ。

Send2Cloudwatch
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
    ],
      "Resource": [
        "arn:aws:logs:*:*:*"
    ]
  }
 ]
}

パッケージ更新、awslogsインストール

console
sudo yum update -y
# ubuntu,centos,redhat はこう
curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O
sudo python ./awslogs-agent-setup.py --region us-east-1
# Amazon Linux ならこっち
# sudo yum install awslogs

バージョン確認

console
$ /var/awslogs/bin/awslogs-version.sh
略
/etc/cron.d/awslogs_log_rotate version:
# Version: 1.4.3
CloudWatch Logs Plugin Version:
You are using pip version 6.1.1, however version 9.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
---
Metadata-Version: 1.1
Name: awscli-cwlogs
Version: 1.4.4
Summary: AWSCLI CloudWatch Logs plugin
Home-page: http://aws.amazon.com/cli/
Author: Amazon
Author-email: UNKNOWN
License: Amazon Software License
Location: /var/awslogs/lib/python2.7/site-packages
Requires: awscli, six, python-dateutil
AWS CLI Version

ログファイル送信設定、先ほどの secure_sshd を指定する

/var/awslogs/etc/awslogs.conf
# 元のmessageログ送信は今回使わないのでコメントアウト
# [/var/log/messages]
# datetime_format = %b %d %H:%M:%S
# file = /var/log/messages
# buffer_duration = 5000
# log_stream_name = {instance_id}
# initial_position = start_of_file
# log_group_name = /var/log/messages

# 普通にsecureログ送るならこう
# [/var/log/secure]
# datetime_format = %b %d %H:%M:%S
# file = /var/log/secure
# buffer_duration = 5000
# log_stream_name = {instance_id}
# initial_position = start_of_file
# log_group_name = /var/log/secure

[/var/log/secure_sshd]
datetime_format = %b %d %H:%M:%S
file = /var/log/secure_sshd
buffer_duration = 5000
log_stream_name = {instance_id}
initial_position = start_of_file
log_group_name = /var/log/secure_sshd

必要に応じてプロキシ設定

/var/awslogs/etc/proxy.conf
HTTP_PROXY=proxyserver:8080
HTTPS_PROXY=proxyserver:8080
NO_PROXY=

サービス再起動&起動設定

console
sudo service awslogs start
sudo chkconfig awslogs on

Logs への送信ログ確認するなら

console
sudo tail /var/log/awslogs.log

動きました

image.png

参考

CloudWatch Logs エージェントのリファレンス – Amazon CloudWatch ログ
クイックスタート: 実行中の EC2 Linux インスタンスに CloudWatch Logs エージェントをインストールして設定する – Amazon CloudWatch ログ
Linux環境設定/sshによる不正アクセスを確認する – Linuxと過ごす
Rsyslog – Wikinote
システム管理の基礎 syslogdの設定をマスターしよう:Linux管理者への道(3) – @IT
必読!ログファイルとディレクトリ | Think IT(シンクイット)

続きを読む

CloudFormationテンプレート(JSON) – CloudTrail 設定

1. 概要

2. 各リソースメモ

3. JSONテンプレート

CloudTrail.json
{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "CloudTrail",
     "Resources" : {
        "S3Bucket" : {
            "Type" : "AWS::S3::Bucket",
            "Properties" : {
                "BucketName" : { "Fn::Join" : [ "", [ "cloudtrail-", { "Ref" : "AWS::AccountId" } ] ] }
            }
        },
        "S3BucketPolicy" : {
            "Type" : "AWS::S3::BucketPolicy",
            "Properties" : {
                "Bucket" : { "Ref" : "S3Bucket"},
                "PolicyDocument" : {
                    "Version" : "2012-10-17",
                    "Statement" : [
                        {
                            "Sid" : "AWSCloudTrailAclCheck20150319",
                            "Effect" : "Allow",
                            "Principal" : { "Service" : "cloudtrail.amazonaws.com"},
                            "Action" : "s3:GetBucketAcl",
                            "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::cloudtrail-", { "Ref" : "AWS::AccountId" } ] ] }
                        },
                        {
                            "Sid" : "AWSCloudTrailWrite20150319",
                            "Effect" : "Allow",
                            "Principal" : { "Service" : "cloudtrail.amazonaws.com" },
                            "Action" : "s3:PutObject",
                            "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::cloudtrail-", { "Ref" : "AWS::AccountId" }, "/AWSLogs/", { "Ref" : "AWS::AccountId" }, "/*" ] ] },
                            "Condition" : { "StringEquals" : { "s3:x-amz-acl" : "bucket-owner-full-control" } }
                        }
                    ]
                }
            }
        },
        "IAMRole" : {
            "Type" : "AWS::IAM::Role",
            "Properties" : {
                "AssumeRolePolicyDocument" : {
                    "Version" : "2012-10-17",
                    "Statement" : [
                        {
                            "Sid" : "",
                            "Effect" : "Allow",
                            "Principal" : {
                                "Service" : "cloudtrail.amazonaws.com"
                            },
                            "Action" : "sts:AssumeRole"
                        }
                      ]
                },
                "Path" : "/",
                "Policies" : [
                    {
                        "PolicyName" : "cloudtrail",
                        "PolicyDocument" : {
                            "Version" : "2012-10-17",
                            "Statement" : [
                                {
                                    "Sid" : "AWSCloudTrailCreateLogStream20141101",
                                    "Effect" : "Allow",
                                    "Action" : [ "logs:CreateLogStream" ],
                                    "Resource" : { "Fn::Join" : [ "", [ "arn:aws:logs:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":log-group:CloudTrail:log-stream:", { "Ref" : "AWS::AccountId" }, "_CloudTrail_", { "Ref" : "AWS::Region" }, "*" ] ] }
                                },
                                {
                                    "Sid" : "AWSCloudTrailPutLogEvents20141101",
                                    "Effect" : "Allow",
                                    "Action" : [ "logs:PutLogEvents" ],
                                    "Resource" : { "Fn::Join" : [ "", [ "arn:aws:logs:", { "Ref" : "AWS::Region" }, ":", { "Ref" : "AWS::AccountId" }, ":log-group:CloudTrail:log-stream:", { "Ref" : "AWS::AccountId" }, "_CloudTrail_", { "Ref" : "AWS::Region" }, "*" ] ] }
                                }
                            ]
                        }
                    }
                ],
                "RoleName": "cloudtrail"
            }
        },
        "LogsLogGroup": {
            "Type" : "AWS::Logs::LogGroup",
            "Properties" : {
                "LogGroupName" : "CloudTrail",
                "RetentionInDays" : 7
            }
        },
        "CloudTrailTrail" : {
            "DependsOn" : "S3BucketPolicy",
            "Type" : "AWS::CloudTrail::Trail",
            "Properties" : {
                "CloudWatchLogsLogGroupArn" : { "Fn::GetAtt" : [ "LogsLogGroup", "Arn" ] },
                "CloudWatchLogsRoleArn" : { "Fn::GetAtt": [ "IAMRole", "Arn" ] },
                "IncludeGlobalServiceEvents" : true,
                "IsLogging" : true,
                "IsMultiRegionTrail" : true,
                "S3BucketName" : { "Fn::Join": [ "", [ "cloudtrail-", { "Ref" : "AWS::AccountId" } ] ] },
                "TrailName" : { "Ref": "AWS::AccountId" }
            }
        }
    },
    "Outputs" : {
        "S3Bucket" : {
            "Description" : "S3 Bucket Name",
            "Value" : { "Ref" : S3Bucket }
        },
        "IAMRole" : {
            "Description" : "IAM Role Name",
            "Value" : { "Ref" : IAMRole }
        },
        "LogsLogGroup" : {
            "Description" : "Log Group Name",
            "Value" : { "Ref" : LogsLogGroup }
        },
        "CloudTrailTrail" : {
            "Description" : "Trail Name",
            "Value" : { "Ref" : CloudTrailTrail }
        }
    }
}

続きを読む

CloudFormationテンプレート(JSON) – AWS Config 設定

1. 概要

2. 各リソースメモ

3. JSONテンプレート

AWSConfig.json

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "AWS Config",
    "Resources" : {
        "S3Bucket" : {
            "Type" : "AWS::S3::Bucket",
            "Properties" : {
                "BucketName" : { "Fn::Join" : [ "", [ "awsconfig-", { "Ref" : "AWS::AccountId" } ] ] }
            }
        },
        "S3BucketPolicy" : {
            "Type" : "AWS::S3::BucketPolicy",
            "Properties" : {
                "Bucket" : { "Ref": "S3Bucket"},
                "PolicyDocument" : {
                    "Version" : "2012-10-17",
                    "Statement" : [
                        {
                            "Sid" : "AWSConfigBucketPermissionsCheck",
                            "Effect" : "Allow",
                            "Principal" : {
                                "Service" : [
                                    "config.amazonaws.com"
                                ]
                            },
                            "Action" : "s3:GetBucketAcl",
                            "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::awsconfig-", { "Ref" : "AWS::AccountId" } ] ] }
                        },
                        {
                            "Sid" : " AWSConfigBucketDelivery",
                            "Effect" : "Allow",
                            "Principal" : {
                                "Service" : [
                                    "config.amazonaws.com"
                                ]
                            },
                            "Action" : "s3:PutObject",
                            "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::awsconfig-", { "Ref" : "AWS::AccountId" }, "/AWSLogs/", { "Ref" : "AWS::AccountId" }, "/Config/*" ] ] },
                            "Condition" : {
                                "StringEquals" : {
                                    "s3:x-amz-acl" : "bucket-owner-full-control"
                                }
                            }
                        }
                    ]
                }
            }
        },
        "SNSTopic" : {
            "Type" : "AWS::SNS::Topic",
            "Properties" : {
                "DisplayName" : "AWS Config Notification Topic",
                "TopicName" : "awsconfig"
            }
        },
        "SNSSubscription" : {
            "Type" : "AWS::SNS::Subscription",
            "Properties" : {
                "Endpoint" : "test@example.com",
                "Protocol" : "email",
                "TopicArn" : { "Ref" : "SNSTopic"}
            }
        },
        "IAMRole" : {
            "Type" : "AWS::IAM::Role",
            "Properties" : {
                "AssumeRolePolicyDocument" : {
                    "Version" : "2012-10-17",
                    "Statement" : [
                        {
                            "Sid" : "",
                            "Effect" : "Allow",
                            "Principal" : {
                                "Service" : "config.amazonaws.com"
                            },
                            "Action" : "sts:AssumeRole"
                        }
                    ]
                },
                "ManagedPolicyArns" : [ "arn:aws:iam::aws:policy/service-role/AWSConfigRole" ],
                "Path" : "/",
                "Policies" : [
                    {
                        "PolicyName" : "awsconfig",
                        "PolicyDocument" : {
                            "Version" : "2012-10-17",
                            "Statement" : [
                                {
                                    "Effect" : "Allow",
                                    "Action" : [
                                        "s3:PutObject*"
                                    ],
                                    "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::awsconfig-", { "Ref" : "AWS::AccountId" }, "/AWSLogs/", { "Ref" : "AWS::AccountId" }, "/*" ] ] },
                                    "Condition" : {
                                        "StringLike" : {
                                            "s3:x-amz-acl" : "bucket-owner-full-control"
                                        }
                                    }
                                },
                                {
                                    "Effect" : "Allow",
                                    "Action" : [ "s3:GetBucketAcl" ],
                                    "Resource" : { "Fn::Join" : [ "", [ "arn:aws:s3:::awsconfig-", { "Ref" : "AWS::AccountId" } ] ] }
                                },
                                {
                                    "Effect" : "Allow",
                                    "Action" : "sns:Publish",
                                    "Resource" : "arn:aws:sns:ap-northeast-1:777813037810:awsconfig",
                                    "Resource" : { "Fn::Join" : [ "", [ "arn:aws:sns:ap-northeast-1:", { "Ref" : "AWS::AccountId" }, ":", { "Fn::GetAtt" : [ "SNSTopic", "TopicName" ] } ] ] }
                                }
                            ]
                        }
                    }
                ],
                "RoleName" : "awsconfig"
            }
        },
        "ConfigConfigurationRecorder" : {
            "Type" : "AWS::Config::ConfigurationRecorder",
            "DependsOn" : [ "S3Bucket", "SNSTopic" ],
            "Properties" : {
                "Name" : { "Fn::Join" : [ "", [ "awsconfig-", { "Ref" : "AWS::AccountId" } ] ] },
                "RecordingGroup" : {
                    "AllSupported" : true,
                    "ResourceTypes" : [],
                    "IncludeGlobalResourceTypes" : true
                },
                "RoleARN" : { "Fn::GetAtt" : [ "IAMRole", "Arn" ] }
            }
        },
        "ConfigDeliveryChannel" : {
            "Type" : "AWS::Config::DeliveryChannel",
            "DependsOn" : [ "S3Bucket", "SNSTopic" ],
            "Properties" : {
                "Name" : { "Fn::Join" : [ "", [ "awsconfig-", { "Ref" : "AWS::AccountId" } ] ] },
                "S3BucketName" : { "Ref" : "S3Bucket" },
                "SnsTopicARN" : { "Ref" : "SNSTopic" }
          }
        }
    },
    "Outputs" : {
        "S3Bucket" : {
            "Description" : "S3 Bucket Name",
            "Value" : { "Ref" : S3Bucket }
        },
        "SNSTopic" : {
            "Description" : "SNS Topic Name",
            "Value" : { "Fn::GetAtt" : [ "SNSTopic", "TopicName" ] }
        },
        "IAMRole" : {
            "Description" : "IAM Role Name",
            "Value" : { "Ref" : IAMRole }
        },
        "ConfigConfigurationRecorder" : {
            "Description" : "Config ConfigurationRecorder Name",
            "Value" : { "Ref" : ConfigConfigurationRecorder }
        },
        "ConfigDeliveryChannel" : {
            "Description" : "Config DeliveryChannel Name",
            "Value" : { "Ref" : ConfigDeliveryChannel }
        }
    }
}

続きを読む

Athenaで入れ子のjsonにクエリを投げる方法が分かりづらかったので整理する

Kinesis FirehoseでS3に置かれた圧縮したjsonファイルを、それに対してクエリを投げる、というのを検証してたのですが、Hive素人なのでスキーマの作り方もクエリの投げ方も正直あんまり良くわかってませんでした。

そこで下記を参照しながらスキーマの作成とクエリ投入をやってみて、最終的にうまくいきました。

日本語記事
https://aws.amazon.com/jp/blogs/news/create-tables-in-amazon-athena-from-nested-json-and-mappings-using-jsonserde/

元記事
https://aws.amazon.com/jp/blogs/big-data/create-tables-in-amazon-athena-from-nested-json-and-mappings-using-jsonserde/

ずーっと日本語記事を読みながらやっていたのですが、これがめちゃくちゃわかりづらい!!!
※理解度には個人差があります

多分知っている人が見たら何となくわかるんでしょうが、恐らくこれを見るのは自分みたいにあまり良く知らないので参考にしながら実際にやってみている、という層だと思います。
最終的に上手く行ってから思ったのは、前提知識がないと読むのがしんどい、ということですね…。
ただもう少し書いといてくれるだけで十分なのに…。
原文も軽く見ましたが、そっちにも書いてないのでそもそも記述されてません。

調べてもまだ中々情報が出てこない上に、クエリ投入時にエラーが出た場合もエラーメッセージが淡白すぎてどこが問題でエラーになってるのかさっぱりわからなくて悪戦苦闘してました。

そんなわけで、今後同じところで困る人が一人でも減るように、自分用メモも兼ねてハマったところについて補足をしておきたいと思います。

概要

リンク先で書いてあることの流れは大まかに下記のとおりです。

  1. FirehoseでSESの送信イベントログをS3に保存する
    送信イベントログはjson形式で、それをFirehoseでS3に保存しています。
  2. Athenaのテーブルを作成して、クエリを投げる
    • ただテーブル作成して投げる場合
    • 入れ子になっているjsonに対してテーブル作成してクエリを投げる場合
    • 禁止文字を含んでいるものに対してテーブルを作成してクエリを投げる場合
      わかりづらいですが、禁止文字を含む項目をマッピングする項目とクエリを投げる項目が分かれています。)
  3. hive-json-schemaの紹介
    jsonからテーブル作成のためのクエリを生成するツールっぽいのですが、紹介してるわりにちっとも使い方が書いてません…。
    使い方の解説をどなたか…。

ハマったところ

入れ子になったjsonに対するテーブル作成について

ハマったところといいつつ、自分はこの辺は割とスムーズに行ったのですが、ちょっとわかりづらいかもしれないので念のため。
サンプルにもありますが、jsonの中にまたjsonとか配列とかが入っている、みたいなケースは多くあります。
そういった場合、内部にあるjsonに対してstruct型を使って、その下の項目について型を定義してやればOKです。
その中にさらにjsonがある場合はさらにその中にstruct型で定義をすればOK。
例にあるものだと、内部にmail{~}とjsonがあり、その中にさらにいくつかのjsonがあるので、それぞれに対してstruct型で定義をしています。
以下引用(全文は貼っていないので、元はリンク先を見てください。)
※一部バッククオート(`)で囲われている項目がありますが、予約語として使われている言葉をそのまま使用するとエラーになるそうです。
そのため、バッククオートで囲うことによってエスケープしてるようです。

抜粋した入れ子の部分
 mail struct<`timestamp`:string,
              source:string,
              sourceArn:string,
              sendingAccountId:string,
              messageId:string,
              destination:string,
              headersTruncated:boolean,
              headers:array<struct<name:string,value:string>>,
              commonHeaders:struct<`from`:array<string>,to:array<string>,messageId:string,subject:string>
              > 

禁止文字そのものについて

まず、禁止文字が色々あることを最初大して理解してませんでした。
項目名(↑の例だと、timestampとかsourceとかのところ)の定義に使用できない文字があります。
記事中だと「:」(コロン)が禁止文字列なので、それがクエリ中の該当箇所に入っているとエラーになります。
あとは「-」(ハイフン)なんかも禁止文字のようです。
例えばHTTPリクエストのログを見たとき、ヘッダとかはハイフンを使った項目がいくつもあったりするので困りますよね。
一応記事中の例では両方「_」(アンダースコア)に変換しています。(コロンにしか触れてませんが…。)
最初は禁止文字があると知らず、なぜエラーになっているかわからずにハマってました。
この辺どっかにまとまってるのかな…?
どうやって回避するかというと、それがWITH SERDEPROPERTIESの部分です。

禁止文字を含む場合のマッピングの仕方について

最初見た時はなんでこんなことをするのかわかりませんでしたが、上記の通り項目名を定義するときに禁止文字が入っているとエラーになります。
なので、WITH SERDEPROPERTIESの項目で、禁止文字列を含んだ項目名を、禁止文字列のない文字列にマッピングし、元のjsonのkeyでは禁止文字列を含んでいたものに対し、テーブル上ではカラム名として別の文字列をあてがうことができます。
記事中では、コロンやハイフンをアンダースコアに変換した文字列にマッピングしています。
式の左側がカラム名に使いたい文字列で、それに対して右側がデータの元の実際の名前です。
"mapping.カラム名に使いたい文字列"="実際の名前" みたいに記述してます。

マッピングの仕方

WITH SERDEPROPERTIES (
  "mapping.ses_configurationset"="ses:configuration-set",
  "mapping.ses_source_ip"="ses:source-ip", 
  "mapping.ses_from_domain"="ses:from-domain", 
  "mapping.ses_caller_identity"="ses:caller-identity"
  )

クエリの投げ方

これもまあおまけで書いておくと、ここまでしっかりと下の項目までテーブルを定義しておくと、下の項目までクエリで引っ張ることが出来ます。
記事中では下記のような例が出ています。

元記事にある例
SELECT eventtype as Event,
         mail.timestamp as Timestamp,
         mail.tags.ses_source_ip as SourceIP,
         mail.tags.ses_caller_identity as AuthenticatedBy,
         mail.commonHeaders."from" as FromAddress,
         mail.commonHeaders.to as ToAddress
FROM sesblog2
WHERE eventtype = 'Bounce'

mail{〜}の下の項目を参照する時は上記のようにドットをつけて該当項目の名前を指定しています。
さらにその下の項目を参照する時はその後ろにさらにドットをつけています。
この辺は直感的にわかりやすいかもしれません。

おまけとかtipsとか

Firehoseで配置されたフォルダ構成ではパーティションを自動で切ってもらえない

hiveではフォルダが/bucketname/path/to/log/year=YY/month=MM/day=dd/foo
みたいな構成だと自動でパーティション設定してくれるらしいのですが、FirehoseでS3にデータ配置すると/bucketname/path/to/log/YYYY/MM/DD/fooみたいになるので、自分でパーティションを作成する必要があります。
パーティションがない状態でクエリを投げても1件も引っかかりません。
これを作るには下記のようなクエリを投げる必要があります。

elbログを対象としたテーブルにパーティションを作成する場合
ALTER TABLE database_name.table_name
ADD PARTITION (year='2016',month='08',day='28')
location 's3://elb-access-log/AWSLogs/00000000000000/elasticloadbalancing/ap-northeast-1/2016/08/28/';

※参考
https://qiita.com/r4-keisuke/items/d3d339b76d4368b6b30a

上記の例だと1日ずつパーティションを設定する必要があるのですが、
パーティション数には上限があるらしい(1テーブル20000まで)ので、1日ずつとか、1時間ずつとかフォルダ分けしている場合はちょっと注意が必要かもしれないです。
※パーティションの上限については下記
https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws_service_limits.html#limits_glue
さすがに対象が多すぎとなるとしんどいので、シェルスクリプトとかで回すといいと思います。
ただ、シェルスクリプト自体も1つ1つの処理実行だとそこそこ時間かかるのと、パーティションを設定するためのクエリでクエリ履歴が埋め尽くされるのが難点です。

データ元にない項目を定義しても値がnullになるだけで問題はない

jsonの出力が一定じゃなくて、いくつかの似たような型のjsonが混ざっていたり、ものによって存在しない項目があったりしても、それらのキーを全て網羅するようにまとめて定義しちゃって問題ないみたいです。
定義したけどデータ元に項目がない場合はnullが入るだけのようで。
逆に元データにある項目を全部定義する必要はないので、元データにあっても使わないような項目はテーブル作成の段階で定義しないようにしてもいいみたいですね。

ざっと書いたので、わかりづらいとか、もっとこうすればみたいな指摘があればいただけると嬉しいです。

続きを読む

EC2で直接動かしていたクローラをECSに移行した話

はじめに

pythonのスクレイピングフレームワークである、scrapyを使って作成したクローラをEC2にdocker-composeをインストールしてcronで定期実行していた。
しかし、デプロイするときや、新しくspiderを作成したときに、いちいちsshしてサーバに入って設定するのが面倒だったので、コンテナ管理ができるECSに移行してみた。
その時のECSの設定とか、ecs-cliについて実際に触ってみたときのメモをまとめる。

ECSってなんなの?

AWSのコンテナ管理サービスでDockerに対応している。
ECSがやっていることは、dockerが入っているAMIからEC2インスタンスを作成して、その上でdockerを動かしている。
詳しい概念や、仕組みについては別記事で結構まとめられたたので参考までに。

Amazon EC2 Container Service(ECS)の概念整理

移行手順

ECSに移行する前の構成

EC2上にdocker-composeをインストールしてcronで動かしていたので、開発用と本番用のdocker-compose.ymlを作成していた。

その時のdocker-compose.ymlがこんな感じ(collecというのはクローラのmodule名)。

version: '3.1'
services:
  splash:
    restart: always
    image: scrapinghub/splash:2.3.3
    container_name: collec_splash
    ports:
      - "5023:5023"
      - "8050:8050"
      - "8051:8051"
  mongo:
    restart: always
    image: mongo:3.4.5
    container_name: collec_mongo
    volumes:
      - ./data/mongo:/data/db
  redis:
    restart: always
    image: redis:3.2.9
    container_name: collec_redis
    volumes:
      - ./data/redis:/data
    command: redis-server --appendonly yes
  scrapy:
    build: .
    container_name: collec_scrapy
    environment:
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    volumes:
      - .:/root/dev
    depends_on:
      - splash
      - mongo
      - redis
    tty: true
    command: rq worker crawler scraper -u http://collec_redis

Pythonクローリング&スクレイピング ―データ収集・解析のための実践開発ガイド―を参考にクローリングとスクレイピングをrqを使って分離している。

本来ならば、クローリングとスクレイピングでコンテナを分けるべきだが面倒くさがって分けていない。
Dockerfileも一つだけで、volumesでソースを読み取り、 docker-compose exec コマンドで実行させるようにしていた。

アンチパターンだらけだったけど、動くから放置していた。
しかし、ECSに移行するにあたり、ちゃんとしたイメージを作る必要があったため、リファクタをすることにした。

ECSに移行するための準備

ecs-compose.ymlの作成

ECSを使用するためには、タスク定義を作成する必要がある。
タスク定義はAWSコンソール上から手動で作成することもできるが、結構めんどくさいので、docker-compose.ymlを使ってタスク定義を作成したかった。
調べてみると、ecs-cliというものが用意されており、これを使えばdocker-compose.ymlを元にタスク定義を作成する事ができる。
しかし、docker-compose.ymlの2系にしか対応していなかったのと、log driverのような専用の指定が必要だったため、ecs-cli用にecs-compose.ymlを作成した。

作成したecs-compose.ymlがこちら(collecはモジュール名)

version: '2'
services:
  splash:
    image: scrapinghub/splash:2.3.3
    ports:
      - "5023:5023"
      - "8050:8050"
      - "8051:8051"
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: splash
  mongo:
    image: mongo:3.4.5
    ports:
      - "27017:27017"
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: mongo
  redis:
    image: redis:3.2.11-alpine
    command: redis-server --appendonly yes
    ports:
      - "6379:6379"
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: redis
  scraper:
    image: {aws-id}.dkr.ecr.ap-northeast-1.amazonaws.com/collec/scraper
    environment:
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    links:
      - mongo:collec_mongo
      - redis:collec_redis
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: scraper
    command: rq worker crawler scraper -u http://collec_redis
  crawler:
    image: {aws-cli}.dkr.ecr.ap-northeast-1.amazonaws.com/collec/crawler
    environment:
      ENV: production
      POSTGRES_HOST: $POSTGRES_HOST
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    logging:
      driver: awslogs
      options:
        awslogs-group: collec
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: crawler
    links:
      - mongo:collec_mongo
      - redis:collec_redis
      - splash:collec_splash
      - scraper:collec_scraper

crawlerとscraperのタスクには自作のdocker imageを使用している。
次に書くが、自作のimageはどこかに上げておかないと使用できない。
上げたimageのURIをecs-compose.ymlのimageに書くことを忘れないこと。

Docker imageの作成

ECSでタスク定義する際には、docker imageをpullしてくるため、どこかにimageをpushしておく必要がある。
1GBまでは無料で使えるので、今回はECRを使用することにした。

AWSコンソール上でECRリポジトリを作成し、pushするだけなので手順は割愛。

ecs-cliを使ったタスク定義の作成

  1. ecs-cliのインストール
    まず、ここの手順に従ってecs-cliをインストールする。

  2. ecs-cliの設定
    次に、ここに従って、ecs-cliの設定を行う。ただ、aws-cliの設定がされているのであれば必要ないので飛ばしても良い。

  3. クラスターの作成
    以下のコマンドを実行すると、ECS上にクラスターが作成される。AWSコンソールでも作成できるが今回はecs-cliで作成する。AWSコンソールでクラスターを作成した場合には、ecs-cli configure --clusterでecs-cliとクラスタを紐付ける必要があるので注意すること。

    $ ecs-cli up --capability-iam --size 1 --instance-type t2.micro
    
  4. タスク定義の作成
    以下のコマンドを実行するとタスク定義が作成される。

    $ ecs-cli compose -f ecs-compose.yml up
    

    AWSコンソールで確認すると、ecs-compose.ymlで定義したコンテナが作成されていることが確認できる。

image.png

タスクのスケジューリング

タスクの実行については割愛する。
AWSコンソールで実際に色々触って試して欲しい。

タスクのスケジューリングは非常に簡単だった。
先程作成したクラスターをAWSコンソール上で選択する。
詳細画面が表示されるので、その中の「タスクのスケジュール」タブを選択し、作成ボタンを押す。

スケジュール作成画面では、実行頻度と、実行したいタスクを入力する必要がある。
先程作成したタスクを選択し、実行頻度は日時にする。cron式で書くと、実行時間も指定できるので、細かく指定したい場合はそちらを選択すると良い。

ちなみに、このスケジュールは内部でCloudWatch Eventsを使用している。
先程のスケジュール作成後、CloudWatch Eventsを見ると、ルールが追加されていることが確認できる。

image.png

所感

EC2上にdocker-composeインストールして、cronで実行していた時に比べると、sshでサーバに入ってデプロイする必要もなくなり、新しいタスクの定義もとても簡単にできるようになった。

最近kubernetesが流行っているので、そちらも触ってみたい。
一応、ECSとkubernetesの連携もできるっぽい。

https://aws.amazon.com/jp/blogs/news/amazon-elastic-container-service-for-kubernetes/

詰まったところ

メモリが足りなくてタスクが実行できない

ecs-compose.ymlから作成したタスク定義からタスクを実行させることができなかった。
原因は、1コンテナのデフォルトメモリ割り当てが512MBだったため、t2.microではメモリが足りず、エラーになってしまうためであった。
インスタンスタイプを上げるか、タスク定義で使用できるメモリの上限を988MBに設定することで、解決することができた(今回は後者を手動でやっている)。

タスク定義が削除できない

AWSコンソールから要らなくなったタスク定義を削除しようとしてもできなかった。
どうやら、Inactiveにしておくことはできるが削除はできないようだった。
この仕様は変わるかもしれないので一旦放置(ここの注記を参照)。

スケジュールした時間にタスクが実行されない?

毎日午前3時にタスクを実行する設定をしていたのだが、ログを見る限りずれた時間に実行されていた。
原因は簡単で、ECSから設定するタスクのスケジュールはUTC時間になるため9時間ずれてしまうことだった。
なので、cron式で書いていた時間を-9時間して解決。
東京リージョンで使ってるんだから、いい感じにやっといてよ…

コンテナが強制終了される?

これは、まだ解決していないのだが、スクレイピング用のコンテナが強制終了されているみたいだった(終了コードも127だし)。
これは、予想だがクローリングとスクレイピングを分けて実行しているので、クローリングが先に終わると、スクレイピングの終了を待たずにコンテナを落としてしまっているのではないかと思う。
ちょっと調査する必要がありそう。

続きを読む

Amazon ECSのログをCloudWatchで収集する

ecs01.png

全体の流れ

  1. ローカルでコンテナを動かす
  2. ECSでコンテナを動かす
  3. ECSでコンテナをクラスタリングする
  4. ECSのログをCloudWatchで収集する ←今回の投稿
  5. HTTPS化する

もくじ

今回の投稿ではコンテナのログをCloudWatchで収集します。

  1. CloudWatchでロググループを作成する
  2. タスク定義でログの設定を行う
  3. ログを閲覧する

1. CloudWatchでロググループを作成する

log01.png
   log02.png

log03.png

2. タスク定義でログの設定を行う

  • ログ設定awslogsを選択する
  • awslogs-groupにロググループ名を入力する
  • awslogs-regionにリージョンコードを入力する
  • awslogs-stream-prefixにプレフィックスを入力する(任意)

参考:awslogs-region
リージョンとアベイラビリティーゾーン – Amazon Elastic Compute Cloud

log04.png

3. ログを閲覧する

log05.png
log06.png

続きを読む

ECSで運用していた社内ツールをFargate化したときに、ハマりやすかった3つのポイント

AWS Fargate Advent Calendar 2017の25日目の記事になります。

他の方が詳細な見解や調査など行われているので、大トリの記事がこれぐらいの内容で大丈夫なのかビビりながら書いています。
ひとまず、年末年始でFargateを試してみようかなあという方の参考になれば。

1.どんな環境をFargate化したか

fargate.png

上記のような構成でECSで運用していた社内ツールのprpr(※)をFargate化しました。
現在のFargateの制限としては、

  1. 東京リージョンがない
  2. SLAがない
  3. 知見が少ない

ということで、production環境にいきなり入れるというよりは、こうしたサービスレベルの低い社内ツールから移行するのがよいかと思います。

※1 SLA設定されてました Amazon Compute サービスレベルアグリーメントを Amazon ECS および AWS Fargate に拡張

※2 prprについては、下記のブログを参照
prprでGithubのPullRequestレビュー依頼をSlack通知する

2.ハマったところ

2-1.FargateがECRのコンテナイメージをpullできない

デプロイしたECSのステータスが、延々とSTOPPEDを繰り返して、ECSのログを見ると、下記のようなエラーが出力され続けているときがありました。残念ながら、Fargate化されても、デプロイ失敗したときなど、ECSが再起動しまくるのは、自前で何とか検知する仕組みを作らないといけなさそう…
ss 2017-12-24 11.51.14.png

解決例:Fargate側にPublicIPを付与する

Fargate: CannotPullContainer located on ECS registry
にもあるように、FargateはVPC内部で起動してくるため、VPC外部への通信経路を確保しておかないと、FargateがECRからコンテナイメージをおとしてくることができません。特にセキュリティ上などで問題なければ、AssignPublicIpを有効化しておきましょう。

CFn例
Service:
    Type: AWS::ECS::Service
    Properties:
        ServiceName: !Ref RoleName
        Cluster: !Ref ECSCluster
        DesiredCount: 1
        LaunchType: FARGATE
        TaskDefinition: !Ref ECSTask
        LoadBalancers:
            -
                ContainerName: !Sub ContainerName
                ContainerPort: 3000
                TargetGroupArn: !Ref ALBTargetGroup
        NetworkConfiguration:
            AwsvpcConfiguration:
                AssignPublicIp: ENABLED
                SecurityGroups:
                    - !Ref ECSSecurityGroup
                Subnets: !Ref SubnetIds

2-2.ECSの動的ポートマッピングは使えない

Fargateのデプロイ中は、下記の画像のような感じで、同一ポートでプライベートIPが異なるという状況になる。Fargateが使用するサブネットでプライベートIPが枯渇したときにどうなるかは未検証。Fargateの起動コンテナ数の制限とかないのであれば、サブネットマスクの設計とかはちょっと注意しておいた方がよさそう。
ss 2017-12-25 7.30.10.png

解決例:ホスト側のポートを固定する

CFn例
ECSTask:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: !Ref FamilyName
        NetworkMode: awsvpc
        RequiresCompatibilities:
            - FARGATE
        Cpu: 256
        Memory: 512
        ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
        ContainerDefinitions:
            -
                Name: !Ref TaskName
                Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RegistoryName}:${ImageTag}"
                PortMappings:
                    -
                        ContainerPort: 3000
                        HostPort: 3000
                Essential: "true"
                Ulimits:
                    -
                        Name: nofile
                        SoftLimit: 65535
                        HardLimit: 65535
                Environment:
                    -
                        Name: PORT
                        Value: 3000
                    -
                        Name: RACK_ENV
                        Value: production
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-group: !Ref CloudWatchLogGroup
                        awslogs-region: !Sub ${AWS::Region}
                        awslogs-stream-prefix: !Ref ImageTag

2-3.CodePipelineでFargateのデプロイを行う、CFnの記述方法がわからない

AWS CodePipeline に Amazon ECS および AWS Fargate のサポートを追加
CodePipeline で ECS にデプロイできるようになり、Docker 環境の継続的デリバリも簡単になりました
にもあるのですが、12/12に、CodePipeline上でFargateのデプロイがサポートされています。
ss 2017-12-25 16.47.19.png
ただ、上記のようなイメージで、CodePipelineとFargateを連携させようとしたときに、CFnのドキュメントからだとCFnでのサンプルが見つけられませんでした。
ひとまず、下記のように書いたら、CFnでも何とか通ったけど、合ってるのかしら(どこかに公式チュートリアルとか準備されてるかな)

解決例:CodeBuildでimagedefinitions.jsonを出力して、CodePipelineのDeployフェーズと連携させる

buildspec.yml例
version: 0.2

phases:
    pre_build:
        commands:
            - $(aws ecr get-login --region $AWS_DEFAULT_REGION)
            - REPOSITORY_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}"
            - IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION}
    build:
        commands:
            - echo Build started on `date`
            - docker build -t "${IMAGE_REPO_NAME}:${IMAGE_TAG}" .
            - docker tag "${IMAGE_REPO_NAME}:${IMAGE_TAG}" "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
    post_build:
        commands:
            - echo Build completed on `date`
            - docker push "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
            - printf '[{"name":"container-name-sample","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
    files:
        - imagedefinitions.json
    discard-paths: yes

imagedefinitions.jsonをCodeBuildで生成したフォルダ直下においておけば、
あとは下記のようなCodePipelineの書き方で、Fargateでもデプロイが可能になる。

CFn例
CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    DependsOn: CodePipelineS3
    Properties:
        Name: codepipeline-sample
        ArtifactStore:
            Type: S3
            Location: !Ref S3BucketName
        RoleArn: !Ref RoleArn 
        Stages:
            -
                Name: Source
                Actions:
                    -
                        Name: Source
                        RunOrder: 1
                        ActionTypeId:
                            Category: Source
                            Owner: ThirdParty
                            Version: 1
                            Provider: GitHub
                        Configuration:
                            Owner: hoge
                            Repo: fuga
                            Branch: master
                            OAuthToken: xxxxxxxxxxxx
                        OutputArtifacts:
                            - Name: Source
            -
                Name: Build
                Actions:
                    -
                        Name: CodeBuild
                        RunOrder: 1
                        InputArtifacts:
                            - Name: Source
                        ActionTypeId:
                            Category: Build
                            Owner: AWS
                            Version: 1
                            Provider: CodeBuild
                        Configuration:
                            ProjectName: !Ref CodeBuild
                        OutputArtifacts:
                            - Name: Build
            -
                Name: Deploy
                Actions:
                    -
                        Name: Deploy
                        ActionTypeId:
                            Category: Deploy
                            Owner: AWS
                            Version: 1
                            Provider: ECS
                        InputArtifacts:
                            - Name: Build
                        Configuration:
                            ClusterName: !Ref ClusterName
                            ServiceName: !Ref ServiceName

3.参考記事

他の方のアドベントカレンダーがすごく参考になったので、CFnまわりの実装で参考にさせていただいた記事をいくつか紹介させていただこうと思います。

Fargate を試した感想と ecs-deploy で Fargate にデプロイできるようにする話
AWS CloudFormationを使ってAWS Fargateの環境を作成してみる
ECS+EC2で動いているサービスをFargateにのせ替える

4.まとめ

社内で使っていたECSをFargate化したことで、EC2の管理(障害対応、およびセキュリティアップデート対応)をなくすことができました。VPCのサブネット設計など、Fargateにしてもインフラ面を意識しないといけないところはありそうなので、また知見がたまれば共有させていただこうと思います。

それでは、皆様メリークリスマス!

続きを読む

CloudFormationを使ってECS環境とそのデプロイシステムを作成する

こんにちは、LIFULLのchissoです。
この記事は、私が勤務するLIFULLのAdvent Calender1の24日目の記事です。

今日はクリスマス・イブですね。みなさまいかがお過ごしでしょうか。
私はQiitaに2本Advent Calenderの記事を上げています。

もう一つはこちらにAthenaの記事を書いています。

さて、早速ですが本題です。
今年の夏頃、AWSで新規サービスを作成する機会がありました。そこで、CloudFormation(以下CFn)を使って、デプロイシステムとElasticContainerService(以下ECS)のサービスを管理する仕組みを作りました。ベースはawslabsが公開しているコチラのリポジトリです。

当時、Qiitaやクラスメソッドさんのブログに大変お世話になりながらなんとかサービスインにこぎつけたのですが、自分なりに要点やハマりどころなど解説したいと思います。

私が当時調べて取り入れたこと、ハマったことなどつらつらと書くのでかなり長いです。
いざやろうと思われた際に参照してもらえれば幸いです。

はじめに

まずはじめに、なぜCFn・ECSを使ったのか簡単に述べておきます。

CFn

こちらは、AWS内のネットワーク・サービス構成もコードで管理したかった、という点につきます。もう流行りというのも憚れますが、Infrastucture as a Codeというやつですね。メリットとしては、

  1. 変更を追跡可能になる

    • 誰がいつ変更したかわからないSecurity GroupやNetwork ACLがなくなる
  2. 開発環境で作った構成がそのまま本番環境で再現できる
    • AWSコンソールはよくできていますが、ポチポチなど人の作業が発生する以上どうしても作業漏れが発生します

思いつくデメリットは、CFn templateの読み書きが辛いことでしょうか。書くときはひたすらAWSの公式ドキュメントとにらめっこです。ただし、慣れてしまえばドキュメントがよくできているので、結構スラスラ書けるようになっていきます。

ECS

こちらは、実行環境を開発環境と本番環境で揃えたい、という点です。またしても解説不要かと思いますが、dockerを使いたいだけです。
Elastic Beanstalk(EB)とどちらを採用するか結構迷ったのですが、ECRを使わないEBではbuildとdeployが一体化してしまうことが気になり、ECRを使うならECSでいいじゃん、となりました。
今年のre:InventでFargateが発表されましたし、ECSにしておいてよかったなと思っているところです。

前提となる知識

CfnとECSがどんなものか、という導入については、優れた記事がすでにたくさんありますので紹介しながら少しだけ補足します。

Cfn

CFnテンプレートは、jsonとyamlの両方で書くことが可能です。

複数のコンポーネントをCFnで管理しようとすると、1つのテンプレートが肥大化してしまいます。しかしaws cliを使うことで、複数のテンプレートに各コンポーネントを定義することが可能です(統合用のテンプレートが必要となります)。

余談ですが、このaws cloudformation packageコマンドは非常に便利で、lambdaファンクションなども参照してアップロードしてくれます。

ECS

ECSについては、はじめ用語が少しわかりにくいですが、下記の認識で良いと思います。

用語 説明
クラスター サービスが稼働するEC2インスタンス(群)
サービス タスクとクラスター、ELB(ターゲットグループ)を紐付ける。EBでいうアプリケーションのような感じ。
タスク コンテナの集まりで、docker-compose.ymlのイメージ。

CfnでECSのデプロイシステムを構築する

本題に入ります。

ベースはawslabsのリポジトリに公開されているものです。
AWSの公式ブログClassMethodさんの記事で、メリットや概略は既に紹介されているので、各yamlの役割や、私が変更を行ったところについて目的と変更方法、また細かいハマりどころを書いていきます。

全体像

ecs-refarch-continuous-deployment.yaml
 |- vpc.yaml
 |- load_balancer.yaml
 |- ecs_cluster.yaml
 |- service.yaml
 - deployment-pipeline.yml

ecs-refarch-continuous-deployment.yamlが全体像(Stack)を定義したCFn templateで、その他のyamlがネストしたStackとなっています。

ecs-refarch-continuous-deployment.yaml内に相対パスで各yamlを記載して、上述したaws cloudformation packageを実行すると、各yamlがS3へアップロードされた上でecs-refarch-continuous-deployment.yamlのパスがS3パスへ書き換えられます。
※ awslabsのecs-refarch-continuous-deployment.yamlはリポジトリ内の子templateを別でS3にあげてあり、初めからそちらを参照するように書かれています。

// before
Cluster:
  Type: AWS::CloudFormation::Stack
  Properties:
    TemplateURL: templates/ecs-cluster.yml
    Parameters:
      VpcId: !Ref VpcId

// after
Cluster:
  Properties:
    Parameters:
      VpcId:
        Ref: VpcId
    TemplateURL: https://s3.amazonaws.com/cfn-templates/{hash値}.template
  Type: AWS::CloudFormation::Stack

また、この構成の場合ecs-refarch-continuous-deployment.yamlがWeb console(またはAPI)から実行するテンプレートとなり、その際に各種パラメータ(Parametersブロックで定義したもの)を与えます。
ecs-refarch-continuous-deployment.yamlからネストしたテンプレートへのパラメータの引き渡しや、ネストしたテンプレート間での値のやりとりもecs-refarch-continuous-deployment.yaml上で行われることになります。

VPC

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/vpc.yaml

VPC, route table, subnetsなど、ネットワーク周りの基本的な定義を行っています。

私は既存のVPC内にECSクラスタを使いたかったため、こちらは利用しませんでした。
OutputsにSubnetsとVpcIdがある通り、別templateでそれらを利用しますが、既存VPC/Subnetのidが利用可能なので、Parametersブロックに定義して渡すようにしています。

LoadBalancer

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/load-balancer.yaml

LoadBalancerと、紐付けるTargetGroupやSecurityGroupを定義しています。

こちらも、初め利用しようとしていましたが最終的にやめました。
というのも、CFnテンプレートを更新した際に、ELB以外のStackが更新される場合は問題ありませんが、Stackの作り直しやELB Stackの更新が発生すると、ELBのURLが変わってしまいます。

実際にELBをCFnで管理するのであれば、Route53もCFn内で管理して、ELBのエンドポイントに対してCNAMEレコードを作成する必要があると思います。
※ 私が作ったものは社内利用のみだったため、固定されたURLさえあればよく、ELBを固定してパラメータでStackに与えることで対応しました。

ECS Cluster

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/ecs-cluster.yaml

このあと定義する、ECSのサービスを稼働させるインスタンスや、AutoScalingGroupの定義を行っています。(Fargateになればいらなくなるはずのテンプレートです。)

Resoures.SecurityGroup

デフォルトでは、SecurityGroupIngressのIpProtcolに-1が設定されており、ELBのSecurityGroupに対してすべてのポートが解放されています。
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/APIReference/API_AuthorizeSecurityGroupIngress.html
私は、明示的にポート解放したかったことと、有事の際にsshでインスタンスにログインして調査を行うために、下記の通り変更しました。

SecurityGroupIngress:
  # hostポートが動的なため、ELBに対してエフェメラル開放
  - SourceSecurityGroupId: !Ref SourceSecurityGroup
    IpProtocol: TCP
    FromPort: 1024
    ToPort: 65535
  # ClusterSshBastionSecurityGroupは踏み台サーバーのSecurityGroup
  - SourceSecurityGroupId: !Ref ClusterSshBastionSecurityGroup
    IpProtocol: TCP
    FromPort: 22
    ToPort: 22

Service

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/service.yaml

ECSのサービス、タスクを定義しています。実際のアプリケーション周りの設定はここで行います。
例えば、コンテナで設定したい環境変数はEnvironmentブロックで定義します。

containerのログをCloudWatchに送る

こちらのスニペットにある通りですが、下記の設定を行うことでコンテナのログをCloudWatchへ送ることができます。

Resources:
  CloudWatchLogs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${AWS::StackName}
      RetentionInDays: 14 # 2週間

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${AWS::StackName}
      ContainerDefinitions:
        - LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Sub ${AWS::StackName}
              awslogs-region: ap-northeast-1
              awslogs-stream-prefix: hogehoge
    # その他propertyは一旦省略

LogGroupはなんでもよいですが、私はStackNameにしました。既存のものを流用するのであれば、上記のCloudWatchLogsのブロックは不要です。

DeploymentPipeline

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/deployment-pipeline.yaml

今回の肝になる部分です。
Github -> CodePipeline -> CodeBuild -> ECS
の流れを自動化するCFn stackをつくります。

概ねサンプルのままで動きますが、ハマりどころが多かったです。

Pipelineブロックのイメージ

私が当時ほとんどCodePipelineを使ったことがなかったためでもありますが、templateだけ見ているとイメージがかなり掴みづらかったので、少し解説します。

1. Pipeline.Stages[0](Name: Source)

Githubからソースをクローンしてきます。
そして、ArtifactStoreで定義したLocationに{なにかのhash値}.zip(※)という名前で保存します。

正直、このサンプル(というかCFnの仕様?)の一番意味がわからないところです。

このName: SourceのOutputArtifactsはAppで、Name: BuildのStage(CodeBuild)に対してInputArtifactとしてAppを渡しています。
awslabsのyamlだと、そのInputArtifactを無視して、別ブロックで定義されたCodeBuildProjectsに記載の通りArtifactとしてBucketを直参照した上で、${ArtifactBucket}/source.zipを参照してCodeBuildを実行しています。
でも、source.zipなんてS3にはないんです、、、

CodeBuildのドキュメントには、

  • source-location: 必須値 (CODEPIPELINE に source-type を設定しない場合)。指定されたリポジトリタイプのソースコードの場所。

    • Amazon S3 では、ビルド入力バケット名の後に、スラッシュ (/) が続き、ソースコードとビルド仕様 (例: bucket-name/object-name.zip) を含む ZIP ファイルの名前が続きます。これは ZIP ファイルがビルド入力バケットのルートにあることを前提としています。(ZIP ファイルがバケット内のフォルダにある場合は、代わりに bucket-name/path/to/object-name.zip を使用してください)。

と記載されていますが、object-nameってなんやねん:joy:
でもこのままでうまく動きます。source.zip以外にしてみたことはないので、どうなってるのかはよくわかりません、、

2. Pipeline.Stages[1](Name: Build)

githubから取得したソースを元に、CodeBuildProjectブロックに記載の通りにdocker buildを実行します。そしてできあがったコンテナをECRにpushします。

はまりどころというわけでもないですが、私が一瞬こんがらがった点を少し。冷静になってみると当たり前なんですが、CodeBuildProjectのEnvironmentブロックは、docker buildを行うdockerコンテナ上の環境変数です。dockerfile内で使えるENVや、最終的なdockerコンテナ(タスク)に渡される環境変数ではありません。Dockerfileで外から変数を受取りたい場合、build.commandsのdockerコマンドに--build-args HOGE=hogeなどとargsを渡してください。

3. Pipeline.Stages[2](Name: Deploy)

ECRのリポジトリがタグ付きで更新されているので、ConfigurationにそのURIなど含めることでいい感じにタスク定義が更新され、service.ymlで定義したスタックが更新されます。(多分)

多分、と書いたのは、実は私は別の方法をとっています。
ここのConfigurationの記述、探したかぎりドキュメントが見つからず、、

- Name: Deploy
  Actions:
    - Name: Deploy
      ActionTypeId:
        Category: Deploy
        Owner: AWS
        Version: 1
        Provider: ECS
      Configuration:
        ClusterName: !Ref Cluster
        ServiceName: !Ref Service
        FileName: images.json
      InputArtifacts:
        - Name: BuildOutput
      RunOrder: 1

私は複数のコンテナをTaskDefinitionに含めており、1つのURIでは対応できませんでした。images.jsonをいい感じに書き換えればうまくいくのか、、わからなかったため、下記の方法をとりました。

複数コンテナを含むタスクをCodePipelineからアップデートする

やったことは、service.yamlをdeployment-pipeline.yamlにネストさせて、DeployアクションでStackの更新を行うようにしました。

- Name: Deploy
  Actions:
    - Name: Deploy
      ActionTypeId:
        Category: Deploy
        Owner: AWS
        Version: 1
        Provider: CloudFormation
      Configuration:
        ChangeSetName: Deploy
        ActionMode: CREATE_UPDATE
        StackName: !Sub "${AWS::StackName}-Service"
        Capabilities: CAPABILITY_NAMED_IAM
        # githubからcloneしたファイルをパス指定
        TemplatePath: App::cfn_templates/packaged_service.yml
        RoleArn: !GetAtt CloudFormationExecutionRole.Arn
        ParameterOverrides: !Sub |
          {
            "Tag" : { "Fn::GetParam" : [ "BuildOutput", "build.json", "tag" ] },
            "DesiredCount": "${TaskDesiredCount}",
            "Cluster": "${Cluster}",
            "TargetGroup": "${TargetGroup}",
            "EcrRepositoryPrefix": "${EcrRepositoryPrefix}",
            "Environment": "${Environment}",
            "S3Bucket": "${S3Bucket}"
          }
      InputArtifacts:
        - Name: App
        - Name: BuildOutput
      RunOrder: 1

軽く解説を。

  • 事前にservice.yamlはaws cloudformation packageコマンドでpackageしておきます。
  • テンプレートはgithubから取得したリポジトリに含んでいるため、InputArtifact経由でのパス指定を行います。
  • ECR Repositoryのprefixと、buildした際のタグをservice.yamlにパラメータとして与えます
    • service.yaml側では下記のような形で、タグを含むURIを指定しておきます。
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: !Sub ${AWS::StackName}
    ContainerDefinitions:
      - Name: hoge
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepositoryPrefix}/hoge:${Tag}
            - Name: fuga
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepositoryPrefix}/fuga:${Tag}

これで、CodePipelineからCloudFormationのCREATE_UPDATEが行われ、めでたくTaskDefinitionの更新->Serviceの更新という順でDeployが行われます。

はまりどころとしては、ParameterOverridesのブロックが、1000文字しか使えません。もろもろ変数展開されたあとだと、結構簡単に1000文字超えて、

Please provide a valid JSON with maximum length of 1000 characters.

って言われます。
Forumにこんなissueがある程度で、(当時)ドキュメントは見つかりませんでした。

私は悩んだ末、はじめParameterOverridesに書いていた値を、yaml_vaultを使ってリポジトリ内で暗号化して管理するようにしました。

おわりに

すごくながくなりました。疲れました。
細かい所見直せてないかもしれませんが、クリスマスイブにこれ以上は悔しいので一度公開します。

このあたりは後日編集されるかもしれません。。

いろいろと個人的な感情が入っていますが、CloudFormationで環境作ってみたいなーという方の参考になれば幸いです。
えっ、Deploy周りはそんなことしないでCircle CI使えって?あーあー聞こえないー。
(一応今回はAWS内で完結することを目標にしてこの形に落ち着いています)

続きを読む

CloudWatchLogsを使用する

EC2(Linux)上のログをCloudWatchLogsへ連携する

1.対象EC2のIAMにポリシー追加

policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        }
    ]
}

2.Linuxサーバで次のシェルを実行

※リージョンの指定とawslogs.confの作成部分を編集して下さい
LaunchConfigurations.sh
#リージョン指定
REGION_NAME='ap-northeast-1'

mkdir /etc/awslogs/
cd /etc/awslogs
curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O

cat << 'EOW' >/etc/awslogs/awslogs.conf

# 下記URLの公式ドキュメント見てawslogs.confを作成してください
#http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/AgentReference.html
[general]
state_file = /var/awslogs/state/agent-state

[file_name_1]
file = 
log_group_name = 
log_stream_name = 
#datetime_format = #指定しない場合は、自動で読み取ってくれる
#multi_line_start_pattern = {datetime_format} #トレースが出力されるログの場合は必要

[file_name_2]
file = 
log_group_name = 
log_stream_name = 

EOW

chmod +x /etc/awslogs/awslogs-agent-setup.py
chmod +x /etc/awslogs/awslogs.conf
./awslogs-agent-setup.py -n -r ${REGION_NAME} -c awslogs.conf
chkconfig awslogs on

AWSコンソールから確認すると2.で指定したロググループとログストリームが作成されているので、以下の様な事ができます。

・連携したログを監視(ERRORが出たらメール通知など)する
・障害調査に使用する

続きを読む

AWS FargateでサーバーレスZabbix

cloudpack大阪の佐々木です。

概要

Zabbixを使うとなると、サーバの面倒はどうするんだ?って話に絶対なるので、AWS Fargateで動かして、サーバーレス化してしまおうということです。
(実際はサーバーレスってい言えるかどうか分かりませんが・・・)
実用に耐えうるかは考慮してません。

構成イメージ

Untitled(1).png

IPが変わるのもどうかなーということで、NLB、ALBを使ってホスト名でアクセスできるようにします。
ZabbixサーバはNLBで、ZabbixWebの方はALBで受けるので、サービスを分けています。

手順

VPCまわり、クラスタ、RDS、NLB、ALBの手順は省略します。
CloudWatch Logsのロググループも先に作っておく必要があります。

ファイル構成

. ─┬─ server ─┬─ docker-compose.yml
   │          └─ ecs-params.yml
   │
   └─ web ────┬─ docker-compose.yml
              └─ ecs-params.yml

zabbix-server

コンフィグファイル

server/docker-compose.yml
version: '2'
services:
  zabbix-server:
    image: zabbix/zabbix-server-mysql
    ports:
      - "10051:10051"
    environment:
      DB_SERVER_HOST: <RDSのエンドポイント>
      MYSQL_DATABASE: <RDSに設定したDB名>
      MYSQL_USER: <RDSに設定したユーザー名>
      MYSQL_PASSWORD: <RDSに設定したパスワード>
    logging:
      driver: awslogs
      options:
        awslogs-group: <ロググループ名>
        awslogs-region: us-east-1
        awslogs-stream-prefix: zabbix
server/ecs-params.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 0.5GB
  services:
    zabbix-server:
      essential: true

run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <サブネットID>
        - <サブネットID>
      security_groups:
        - <セキュリティグループ>
      assign_public_ip: ENABLED

起動

$ cd server
$ ecs-cli compose service up --launch-type FARGATE \
 --target-group-arn <NLBのターゲットグループのARN> \
 --container-name zabbix-server \
 --container-port 10051

zabbix-web

コンフィグファイル

web/docker-compose.yml
version: '2'
services:
  zabbix-web:
    image: zabbix/zabbix-web-apache-mysql
    ports:
      - "80:80"
    environment:
      DB_SERVER_HOST: <RDSのエンドポイント>
      MYSQL_DATABASE: <RDSに設定したDB名>
      MYSQL_USER: <RDSに設定したユーザー名>
      MYSQL_PASSWORD: <RDSに設定したパスワード>
      PHP_TZ: Asia/Tokyo
      ZBX_SERVER_HOST: <zabbix-serverのホスト名(NLBのFQDN)>
      ZBX_SERVER_NAME: <zabbixの管理画面に表示される名前>
      ZBX_SERVER_PORT: 10051
    logging:
      driver: awslogs
      options:
        awslogs-group: <ロググループ名>
        awslogs-region: us-east-1
        awslogs-stream-prefix: zabbix

起動

$ cd web
$ ecs-cli compose service up --launch-type FARGATE \
 --target-group-arn <ALBのターゲットグループのARN> \
 --container-name zabbix-web \
 --container-port 80

接続

ブラウザから http://(ALBのFQDN) にアクセスするとWeb管理画面にログインできます。

image.png

ダッシュボードでのステータスも正常です。

image.png

エージェントで指定する Server はNLBのFQDNになります。

まとめ

Zabbixは単純にスケールアウトできるものでもないですので、あまりFargateに適したものではないですが、サーバのメンテは誰がやる?って話が進まないようなときは、とりあえずFargate化してしまえばいいんではないでしょうか?

続きを読む