[AWS] ECS on EC2 スポットインスタンス + CloudFront で SSL付きサービスを立ち上げてみた
モチベーション
EC2 スポットインスタンス超安いらしい。目的とするサービスは数分くらい止まっても問題なし。
CloudFront 使うと ACM で SSL つけられるらしい。
(ELBでもSSLできるが高い。またECSインスタンス1つしか使わないのでLoadBalancer機能は過重)
やってみた。
立ち上げるサービス概要
- Docker コンテナ
- 主にモバイル端末から画像を受け取り、画像処理して後方サービス(別EC2)に投げる
- 画像処理に AWS 外の他サービスを使う
- クライアントへの処理状況通知に、WebSocket を使う
- 特定施設でのみ利用。1日アクセス数 50 ~ 100 程度
コスト
EC2 (スポットインスタンス) t3a.micro -> $0.0029/1時間 = $2.088/月
CloudFront data transfer 10GB/月 -> $1.74/月
(その他もろもろあるかもしれないが無視できる範囲?)
Total $3.824/月 = 500円くらい?
図(ひどいので後で直す)
事前準備
- Route53 でドメイン準備。ACM で証明書も用意しておく。
- CloudFront のオリジンとなる ElasticIP を一つ作っておく。
- ECR に Docker イメージ入れておく。
CloudFormation 要所
Reseruces から抜粋、一部改変。
VPC周り
普通に VPC 作成、InternetGateway をアタッチ。
VPC には後で使うため Name タグを付与。
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub ${TagApp}-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${TagApp}-igw
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
Subnet
ECS インスタンス1つ動かすだけなので、パブリックサブネット1つだけ。
InternetGateway に抜ける route を設定。
(パブリックサブネットのセキュリティリスク?よく理解していない)
PublicSubnet1RouteTable:
Type: AWS::EC2::RouteTable
DependsOn: AttachGateway
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${TagApp}-public-subnet-1-route-table
PublicSubnet1Route:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref PublicSubnet1RouteTable
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicSubnet1RouteTable
PublicSubnet1:
Type: AWS::EC2::Subnet
DependsOn: AttachGateway
Properties:
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: true
VpcId: !Ref VPC
AvailabilityZone: !Ref AZ1
Tags:
- Key: Name
Value: !Sub ${TagApp}-public-subnet-1
ECS クラスタ
普通に ECS クラスタを作成
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${TagApp}-cluster
ClusterSettings:
- Name: containerInsights
Value: enabled
セキュリティグループ
SpotFleet(ECSインスタンス)で使う用。依存が多いのでecs.yaml内で定義。
インはSSH(22)とDockerコンテナがexposeしているポート(8080)をオープン。
アウトは全オープン。
SpotInstanceSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: Spot Instance Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- CidrIp: !Ref SourceCidr
FromPort: 22
ToPort: 22
IpProtocol: tcp
- CidrIp: !Ref SourceCidr
FromPort: 8080
ToPort: 8080
IpProtocol: tcp
SecurityGroupEgress:
- CidrIp: !Ref SourceCidr
FromPort: 0
ToPort: 0
IpProtocol: '-1'
CloudFront
GUI で作ったものを yaml へ変換しただけ。精査していない...
事前に作った Elastic IP を Origin に設定。
ViewerCertificate -> AcmCertificateArn に事前に作った ACM の ARN を設定
あと、WebSocket通すために Policy 何か調整したかも...
CloudFrontDistribution:
Type: "AWS::CloudFront::Distribution"
Properties:
DistributionConfig:
Aliases:
- !Ref Domain
Origins:
-
ConnectionAttempts: 3
ConnectionTimeout: 10
CustomOriginConfig:
HTTPPort: !Ref ContainerPort
HTTPSPort: 443
OriginKeepaliveTimeout: 5
OriginProtocolPolicy: "http-only"
OriginReadTimeout: 30
OriginSSLProtocols:
- "TLSv1"
- "TLSv1.1"
- "TLSv1.2"
DomainName: !Ref ElasticIpDomain
Id: !Ref ElasticIpDomain
OriginPath: ""
DefaultCacheBehavior:
AllowedMethods:
- "HEAD"
- "DELETE"
- "POST"
- "GET"
- "OPTIONS"
- "PUT"
- "PATCH"
CachedMethods:
- "HEAD"
- "GET"
Compress: true
CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
OriginRequestPolicyId: "216adef6-5c7f-47e4-b989-5492eafa07d3"
ResponseHeadersPolicyId: "eaab4381-ed33-4a86-88ca-d9558dc6cd63"
SmoothStreaming: false
TargetOriginId: !Sub "ec2-xxx-xxx-xxx-xxx.${AWS::Region}.compute.amazonaws.com"
ViewerProtocolPolicy: "redirect-to-https"
Comment: ""
PriceClass: "PriceClass_200"
Enabled: true
ViewerCertificate:
AcmCertificateArn: !Ref AcmCertificateArn
CloudFrontDefaultCertificate: false
MinimumProtocolVersion: "TLSv1.2_2021"
SslSupportMethod: "sni-only"
Restrictions:
GeoRestriction:
RestrictionType: "none"
HttpVersion: "http2"
DefaultRootObject: ""
IPV6Enabled: true
SpotFleet
スポットインスタンスを1つ確保するための Spot Fleet。
UserData:
のスクリプトで aws-cfn-bootstrap
をインストール。
Metadata -> AWS::CloudFormation::Init -> commands
で ECS_CLUSTER
にクラスタ名を入れて ECS クラスタに参加させている。
また、aws ec2 associate-address ...
で Elastic IP 紐づけ自動化。
スポットインスタンスが終了して再度別インスタンス立ち上がるときも同じEIPを使って起動するため、CloudFront の Origin はこの Elastic IP を指定すればよい。
下記以外に IAM 系リソースあるが割愛。
SpotFleet:
Type: AWS::EC2::SpotFleet
DependsOn:
- SpotFleetRole
- SpotFleetInstanceProfile
Properties:
SpotFleetRequestConfigData:
AllocationStrategy: diversified
IamFleetRole: !GetAtt SpotFleetRole.Arn
LaunchSpecifications:
- IamInstanceProfile:
Arn: !GetAtt SpotFleetInstanceProfile.Arn
ImageId: !FindInMap [ AWSInstanceTypeToAMI, !Ref InstanceType, AMI ]
InstanceType: !Ref InstanceType
KeyName: !Ref KeyName
Monitoring:
Enabled: true
SecurityGroups:
- GroupId: !Ref SpotInstanceSecurityGroupId
SubnetId: !Ref Subnet1Id
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo ECS_CLUSTER=${TagApp}-cluster >> /etc/ecs/ecs.config
amazon-linux-extras install epel -y
yum -y update
yum -y install aws-cfn-bootstrap aws-cli jq unzip
aws configure set default.region ${AWS::Region}
/opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource SpotFleet
TargetCapacity: !Ref SpotFleetTargetCapacity
TerminateInstancesWithExpiration: 'true'
Metadata:
AWS::CloudFormation::Init:
config:
files:
"/etc/cfn/cfn-hup.conf":
mode: 000400
owner: root
group: root
content: !Sub |
[main]
stack=${AWS::StackId}
region=${AWS::Region}
"/etc/cfn/hooks.d/cfn-auto-reloader.conf":
content: !Sub |
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.SpotFleet.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource SpotFleet
commands:
com1-setup-ecs-cluster:
command: !Sub "echo ECS_CLUSTER=${TagApp}-cluster >> /etc/ecs/ecs.config"
com2-associate-eip:
command: !Sub "aws ec2 associate-address --instance-id `/usr/bin/curl -s http://169.254.169.254/latest/meta-data/instance-id` --allocation-id ${ElasticIpId} --allow-reassociation"
services:
sysvinit:
cfn-hup:
enabled: true
ensureRunning: true
files:
- /etc/cfn/cfn-hup.conf
- /etc/cfn/hooks.d/cfn-auto-reloader.conf
ECS タスク / サービス
LaunchType は Fargate ではなく EC2。
ELB 使わないので、ネットワークモードは bridge。
ECR にある Docker イメージ使う。
TaskDefinition:
Type: AWS::ECS::TaskDefinition
DependsOn:
- TaskExecutionRole
- TaskRolePolicyForS3
Properties:
Family: !Sub ${TagApp}-task-def
RequiresCompatibilities: [ EC2 ]
Cpu: 256
Memory: 512
NetworkMode: bridge
ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
TaskRoleArn: !GetAtt TaskRole.Arn
ContainerDefinitions:
- Name: !Sub ${TagApp}-container
Image: !Ref ECRImage
Essential: true
PortMappings:
- ContainerPort: !Ref ContainerPort
HostPort: !Ref ContainerPort
Protocol: tcp
Environment:
- Name: PORT
Value: !Ref ContainerPort
# ... 省略 ...
Service:
Type: AWS::ECS::Service
Properties:
Cluster: !Ref ECSClusterArn
ServiceName: !Sub ${TagApp}-service
TaskDefinition: !Ref TaskDefinition
SchedulingStrategy: REPLICA
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: !Ref DesiredCount
LaunchType: EC2
起動
ecs.yaml、spotfleet.yaml、cloudfront.yaml、service.yaml の順にCloudFormation で スタック作成。
無事サービス起動できた。
今後
しばらく動かしてみてコストが目論見通りかチェック。
セキュリティ強化。
自前 NGINX 挟んで LoadBalancing?
Discussion