AWSの起動テンプレートに記載したインスタンスのUserdataを使用して、インスタンス起動時からログをAmazon S3に配信するCloudFormationを作成する機会があったため、執筆します。
ハマったところや注意点をナレッジとして役立てていただければと思います。
AWS初学者
EC2のログ転送を検討している方
CloudFormationに興味のある方
AWSTemplateFormatVersion: 2010-09-09
Resources:
mainVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
igwName:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref mainVPC
InternetGatewayId: !Ref igwName
PublicRouteTable:
Type: AWS::EC2::RouteTable
DependsOn: AttachGateway
Properties:
VpcId: !Ref mainVPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref igwName
PublicSubnet1:
Type: AWS::EC2::Subnet
DependsOn: AttachGateway
Properties:
CidrBlock: 10.0.210.0/24
AvailabilityZoneId: 'use1-az2'
MapPublicIpOnLaunch: 'True'
VpcId: !Ref mainVPC
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2:
Type: AWS::EC2::Subnet
DependsOn: AttachGateway
Properties:
CidrBlock: 10.0.220.0/24
AvailabilityZoneId: 'use1-az4'
MapPublicIpOnLaunch: 'True'
VpcId: !Ref mainVPC
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.0/24
AvailabilityZoneId: 'use1-az2'
MapPublicIpOnLaunch: 'false'
VpcId: !Ref mainVPC
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateSubnet1RouteTable
PrivateSubnet1RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref mainVPC
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.2.0/24
AvailabilityZoneId: 'use1-az4'
MapPublicIpOnLaunch: 'false'
VpcId: !Ref mainVPC
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateSubnet2RouteTable
PrivateSubnet2RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref mainVPC
PublicSecGroupName:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: handson-lbsg
GroupDescription: SecGroupLB
VpcId: !Ref mainVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
PrivateSecGroupName1:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: handson-websg
GroupDescription: SecGroupWEB
VpcId: !Ref mainVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
PrivateSecGroupName2:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: handson-endppointsg
GroupDescription: SecGroupDB
VpcId: !Ref mainVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 10.0.0.0/16
ssmEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .ssm
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
VpcId: !Ref mainVPC
VpcEndpointType: Interface
SecurityGroupIds:
- !Ref PrivateSecGroupName2
PrivateDnsEnabled: true
ssmmessagesEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .ssmmessages
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
VpcId: !Ref mainVPC
VpcEndpointType: Interface
SecurityGroupIds:
- !Ref PrivateSecGroupName2
PrivateDnsEnabled: true
ec2messagesEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .ec2messages
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
VpcId: !Ref mainVPC
VpcEndpointType: Interface
SecurityGroupIds:
- !Ref PrivateSecGroupName2
PrivateDnsEnabled: true
logsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .logs
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
VpcId: !Ref mainVPC
VpcEndpointType: Interface
SecurityGroupIds:
- !Ref PrivateSecGroupName2
PrivateDnsEnabled: true
MonitoringEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .monitoring
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
VpcId: !Ref mainVPC
VpcEndpointType: Interface
SecurityGroupIds:
- !Ref PrivateSecGroupName2
PrivateDnsEnabled: true
ec2Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .ec2
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
VpcId: !Ref mainVPC
VpcEndpointType: Interface
SecurityGroupIds:
- !Ref PrivateSecGroupName2
PrivateDnsEnabled: true
s3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Join
- ''
- - com.amazonaws.
- !Ref 'AWS::Region'
- .s3
VpcId: !Ref mainVPC
RouteTableIds:
- !Ref PrivateSubnet1RouteTable
- !Ref PrivateSubnet2RouteTable
EC2Role:
Type: AWS::IAM::Role
Properties:
RoleName: handson-webRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/CloudWatchAgentAdminPolicy
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
- arn:aws:iam::aws:policy/AmazonSSMFullAccess
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref EC2Role
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: handson-launchtemplates
LaunchTemplateData:
IamInstanceProfile:
Arn: !GetAtt InstanceProfile.Arn
SecurityGroupIds:
- !Ref PrivateSecGroupName1
- !Ref PrivateSecGroupName2
ImageId: ami-0b5eea76982371e91
InstanceType: t2.micro
Monitoring:
Enabled: true
UserData:
Fn::Base64: |
#!/bin/bash
yum update -y
yum install httpd -y
touch /var/www/html/index.html
echo "Hello World from user data" > /var/www/html/index.html
systemctl enable httpd
systemctl start httpd
amazon-linux-extras enable collectd
yum clean metadata
yum -y install collectd
yum -y install amazon-cloudwatch-agent
systemctl enable amazonn-cloudwatch-agent.service
systemctl start amazonn-cloudwatch-agent.service
cat << EOF > /opt/aws/amazon-cloudwatch-agent/bin/config.json
{
"agent": {
"run_as_user": "root"
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/messages",
"log_group_name": "handson-log-group",
"log_stream_name": "/var/log/messages",
"retention_in_days": -1
}
]
}
}
},
"metrics": {
"metrics_collected": {
"statsd": {
"metrics_aggregation_interval": 60,
"metrics_collection_interval": 10,
"service_address": ":8125"
}
}
}
}
EOF
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json -s
systemctl restart amazonn-cloudwatch-agent.service
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MaxSize: '1'
MinSize: '1'
DesiredCapacity: '1'
TargetGroupARNs:
- !Ref TargetGroup
VPCZoneIdentifier:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref mainVPC
Protocol: HTTP
Port: 80
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: handson-alb
Scheme: internet-facing
SecurityGroups:
- !Ref PublicSecGroupName
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
Port: 80
Protocol: HTTP
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: handson-log-group
FirehoseSubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
Properties:
DestinationArn: !GetAtt Deliverystream.Arn
FilterPattern: ''
LogGroupName: !Ref LogGroup
RoleArn: !GetAtt LogsRole.Arn
LogsRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: !Sub 'logs.${AWS::Region}.amazonaws.com'
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- firehose:PutRecord
- firehose:PutRecords
Resource: !GetAtt 'Deliverystream.Arn'
FirehoseRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ''
Effect: Allow
Principal:
Service: firehose.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: handson-firehose-policy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:AbortMultipartUpload
- s3:GetBucketLocation
- s3:GetObject
- s3:ListBucket
- s3:ListBucketMultipartUploads
- s3:PutObject
Resource:
- !Sub 'arn:aws:s3:::${S3bucket}'
- !Sub 'arn:aws:s3:::${S3bucket}*'
Deliverystream:
Type: AWS::KinesisFirehose::DeliveryStream
Properties:
ExtendedS3DestinationConfiguration:
BucketARN: !Sub 'arn:aws:s3:::${S3bucket}'
RoleARN: !GetAtt 'FirehoseRole.Arn'
S3bucket:
Type: AWS::S3::Bucket
本構成は「米国東部リージョン(バージニア北部)」にて作成しています。
別のリージョンで作成をする際には、上記のyamlテンプレートの「us-east-1」を、任意のリージョンに書き換えてご使用ください。
各サービスの名前を省略しているため、作成されるリソースはランダムな文字列の名前となります。
リソースに任意の名前をつけたい場合は、リソースにNameプロパティを追加し、任意の名前を設定してください。プロパティの記法の確認には下記参考情報に各リソースのドキュメントのリンクを記載しましたのでご活用ください。
アーキテクチャ図では、2つのプライベートサブネットにインスタンスをデプロイしていますが、上記のyamlテンプレートは検証用のため2つあるプライベートサブネットのどちらかに1台のインスタンスがデプロイされるようにしています。
本構成では、Load balancersとAuto Scalling グループで構成されたWebサーバの作成、及び作成したサーバからシステムログを収集し、S3にて保管を想定しています。
まずはシステム構成について説明します。
本構成ではAuto Scalling グループを使用した動的拡張を実施しています。
そのため、Auto Scalling グループが利用する起動テンプレートにWebサービスの起動及びCloudWatch エージェントの設定を記述することで、AutoScalingによって追加されたサーバに改めて設定を行うことなく、ログ収集が行えるようにしています。
Auto Scalling グループを使用した動的拡張のトリガーとして、Application Load balancersとターゲットグループを作成し、ヘルスチェックを行っています。
上記の構成のインスタンスのログをAmazon S3に配信するために今回CloudWatch ロググループとAmazon Kinesis Data Firehoseのサービスを使用しました。
配信の流れは以下の通りです。
インスタンスのログをCloudWatch エージェント経由でCloudWatch ロググループに転送し、Amazon Kinesis Data Firehoseを使用して、CloudWatch ロググループからAmazon S3内のバケットに配信をしています。
続きまして、今回の構成について細かい設定解説をしていきます。長くなるので、インスタンス→CloudWatch ロググループまでと、CloudWatch ロググループ→Amazon S3までに分けて紹介していきます。
まずはインスタンス→CloudWatch ロググループまでの説明です。
インスタンスのログをCloudWatch ロググループへ出力させるためには以下の2点の設定を追加しています。
インスタンスにCloudWatch エージェントのインストール及び設定
インスタンスにCloudWatchへの権限付与
また、本構成ではインスタンスはプライベートサブネットに配置されているため、以下の設定を追加しています。
VPCエンドポイントの作成
セキュリティグループの追加
インスタンスの任意のログをCloudWatchを使用して収集するためには、インスタンスにCloudWatch エージェントをインストール及び設定をする必要があります。
CloudWatch エージェントのインストール及び設定は起動テンプレートのUserdataを利用しました。
yum install amazon-CloudWatch-agent
また、amazon-CloudWatch-agentの設定ファイルを変更することで、収集対象のファイル、転送するCloudWatch ロググループを指定します。
/opt/aws/amazon-CloudWatch-agent/bin/config.json
CloudWatch エージェントの設定は、以下のウィザードを使用した対話方式の設定と、Systems Manager パラメータストア等から設定ファイルを作成する、2種類の方法があります。
amazon-CloudWatch-agent-config-wizard
今回は、Systems Manager パラメータストアを使用せずに、Userdataからcatコマンドを使用してjsonファイルを作成する方法を選択しています。
config.jsonは以下の3つのセクションから構成されます。
上記のUserdataの編集をすることで、用途に合わせて追加のログを収集することができます。
(本構成では/var/log/messageを収集対象としています。)
設定ファイルを作成しただけでは設定は反映がされないため、以下のコマンドでCloudWatch エージェントの設定を更新します。
/opt/aws/amazon-CloudWatch-agent/bin/amazon-CloudWatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-CloudWatch-agent/bin/config.json -s
インスタンスからCloudWatch エージェントを使用してCloudWatch ロググループにログを出力させるためには、EC2がCloudWatchにアクセスする権限が必要です。
以下のIAMポリシーを関連付けたIAMロールを作成しています。
適用ポリシー
また、今回インスタンスに対してSSH接続ではなく、SSMセッションマネージャーを使用したコンソール接続を想定しているため、追加で以下のポリシーも設定しています。
SSMセッションマネージャーを使用しない場合、以下のポリシーは不要です。
作成したロールを起動テンプレート内に関連づけることで、作成したインスタンスがCloudWatchへアクセスすることが可能です。
プライベートサブネットに配置しているインスタンスからCloudWatch ロググループにログを出力するには、VPCエンドポイントを配置して出力経路を確保する必要があります。
そのため、ログ出力用のVPCエンドポイントを作成し、プライベートサブネットに関連付けています。
本構成では米国東部リージョンに作成したため、「us-east-1」としておりますが、こちらを任意のリージョンにすることで他のリージョンでも同様の設定することができます。
インスタンスをパブリックサブネット、プライベートサブネットするに関わらず、CloudWatch エージェント側での追加の設定は不要です。
プライベートサブネットに配置する場合はVPCエンドポイントの設定を忘れないようにしましょう。
VPCエンドポイントはHTTPS(443番ポート)を使用して通信を行うため、インスタンスのセキュリティグループにHTTPS通信を許可する必要があります。
本構成では、区別がしやすいようにインスタンスのセキュリティグループを、Load balancersで使用する外部アクセス用のセキュリティグループと、VPCエンドポイントで使用する内部アクセス用のセキュリティグループに分けて作成しています。
続いてCloudWatch ロググループ→Amazon S3までの設定説明です。
CloudWatch ロググループに出力したログを、Amazon Kinesis Data Firehoseを経由してAmazon S3のバケットに配信するためには、以下の4点を設定します。
ログを格納するAmazon S3バケットの作成
CloudWatch及びAmazon Kinesis Data Firehoseのアクセス権限設定
Amazon Kinesis Data Firehose の作成
CloudWatch ロググループのサブスクリプションフィルターを作成
まずはログを格納するバケットの作成です。
本構成ではバージョニングやライフサイクルポリシーの設定を省略して一意のバケットを作成しています。
CloudWatch ロググループがAmazon Kinesis Data Firehoseにログを配信するためのロールと、Amazon Kinesis Data FirehoseがAmazon S3にログを配信するためのロールの2つを作成しています。
Amazon Kinesis Data Firehose のサービスに対して、単一(PutRecord)または複数(PutRecords)のレコードを送信するポリシー及びロールを作成します。
作成したロールはサブスクリプションフィルターにて関連付けします。
Amazon S3のバケットに対してログを格納するポリシー及びロールを作成します。
作成したロールはAmazon Kinesis Data Firehose にて関連付けします。
前述したAmazon S3バケット及びAmazon Kinesis Data Firehose用のロールを関連付けした、Amazon Kinesis Data Firehose の配信を作成します。
Amazon Kinesis Data FirehoseからAmazon S3に配信を行う場合、ExtendedS3DestinationConfigurationのプロパティタイプを使用します。
CloudWatch ロググループとサブスクリプションフィルターの関連付けを行います。
以上、インスタンス起動時からログをAmazon S3に配信するCloudFormationの解説でした。
CloudFormationの作成経験が浅いため、設定を確認するためにマネジメントコンソールで実際にサービスを作成したり、 AWS公式ドキュメントのパラメータを確認しながら作成をしました。
VPCから作成を行ったため、上記のyamlファイルをコピーペーストすることでCloudFormationで本記事の構成が再現できるようになっています。
ただし、その分yamlファイルが長くなったため、必要な設定値はなるべく最小限にしています。
必要に応じて、以下のAWSドキュメントから必要な設定値を追記してご使用ください。
また、本構成はAWSの無料使用枠以外のサービスも使用しているため、検証時にはリソースの削除漏れにご注意ください。
作成したリソースはCloudFormation上から削除できますが、事前にS3バケットに転送したログの削除をするようにしてください。
AWS::AutoScaling::AutoScalingGroup
AWS::ElasticLoadBalancingV2::TargetGroup
AWS::ElasticLoadBalancingV2::LoadBalancer
AWS::ElasticLoadBalancingV2::Listener
AWS::KinesisFirehose::DeliveryStream
CloudWatch エージェント設定ファイルを手動で作成または編集する