AWSのコンテナ専用OSである Bottlerocket
をECSクラスタに付けてオートスケールしてみました。
この記事ではBottlerocket OSの
aws-ecs-1
というバリアントを利用しますが、このバリアントはdeveloper preview
フェーズなので、今後変わる可能性があります。
Bottlerocketとは
この記事にたどり着く人に伝えるような内容では無いと思うのでAWSのブログのリンク貼っておきます。
実際に見ることが多いのは以下のGithubリポジトリとなります。
ECSでBottlerocketを使うために
Bottlerocketのバリアント
Bottlerocketにはバリアント(Variants)というものがあります。
例えば aws-k8s-1.19
や aws-ecs-1
のようにEKSやECSに必要な準備が整った状態でイメージが提供されています。
最初にも書きましたが、2021年3月30日時点ではECSには aws-ecs-1
というバリアントのみが用意されていて、こちらは Developer Previewのステータスです。
AMIイメージのID取得
BottlerocketのAMIのIDはPublicのSSM Parameter Storeから取得できます。
場所はこちらです。
$ aws ssm get-parameters-by-path --region ap-northeast-1 --path "/aws/service/bottlerocket/aws-ecs-1/x86_64/latest" { "Parameters": [ { "Name": "/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_id", "Type": "String", "Value": "ami-035653083697abe32", "Version": 8, "LastModifiedDate": "2021-03-18T10:30:57.020000+09:00", "ARN": "arn:aws:ssm:ap-northeast-1::parameter/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_id", "DataType": "text" }, { "Name": "/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_version", "Type": "String", "Value": "1.0.7-099d3398", "Version": 8, "LastModifiedDate": "2021-03-18T10:30:56.201000+09:00", "ARN": "arn:aws:ssm:ap-northeast-1::parameter/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_version", "DataType": "text" } ] }
今回必要なのはAMIのIDのみなので、以下のように取得します。
$ aws ssm get-parameter --region ap-northeast-1 --name "/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_id" --query Parameter.Value --output text ami-035653083697abe32
ECSクラスタへの接続設定
TOML形式でユーザデータを渡すと自動で設定してくれます。設定できる項目はこちらに記載があります。
[settings.ecs] cluster = "MyCluster" [settings.ecs.instance-attributes] attribute1 = "foo" attribute2 = "bar"
Proxy設定についても調べて、network.https-proxy
や network.no-proxy
みたいな項目がEKS側は対応されているようです。ECS側のバリアントでは現時点では動かなかったですが将来的に対応されるのではと思います。
何ができて、何ができないか(開発中か)、AWSのコンテナ関連はロードマップが公開されてますが、Bottlerocketについては以下を見る方が良いかもしれません。
Issues · bottlerocket-os/bottlerocket · GitHub
Cloudformationのデプロイ
InternalのALB配下でNGINXを動かす以下の基本構成を調整して作ってみます。
以下のYAMLを保存します。
--- AWSTemplateFormatVersion: "2010-09-09" Description: EKS Managed Node Launch Template Parameters: BottlerocketAmiId: Type: String VpcId: Type: AWS::EC2::VPC::Id ECSSubnets: Type: List<AWS::EC2::Subnet::Id> ALBSubnets: Type: List<AWS::EC2::Subnet::Id> ClusterName: Type: String AllowedPattern: ^[a-zA-Z]*$ Default: BottlerocketCluster ServiceName: Type: String Default: bottlerocket-service AppName: Type: String Default: bottlerocket-nginx EC2InstanceRoleName: Type: String Default: ec2-bottlerocket ALBListenerPort: Type: Number Default: 80 AppContainerPort: Type: Number Default: 80 HostPort: Type: Number Default: 8080 Resources: #--------------------------------- # Bottlerocket #--------------------------------- BottlerocketSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: bottlerocket-sg Tags: - Key: Name Value: bottlerocket-sg GroupDescription: for Bottlerocket VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref HostPort ToPort: !Ref HostPort SourceSecurityGroupId: !GetAtt ALBSecurityGroup.GroupId BottlerocketInstanceRole: Type: AWS::IAM::Role Properties: RoleName: !Ref EC2InstanceRoleName AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: ec2.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role BottlerocketInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - !Ref BottlerocketInstanceRole BottlerocketLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateName: !Sub "ecs-bottlerocket-${ClusterName}" LaunchTemplateData: ImageId: !Ref BottlerocketAmiId InstanceInitiatedShutdownBehavior: terminate IamInstanceProfile: Arn: !GetAtt BottlerocketInstanceProfile.Arn TagSpecifications: - ResourceType: instance Tags: - Key: Name Value: !Sub "bottlerocket-${ClusterName}" SecurityGroupIds: - !Ref BottlerocketSecurityGroup UserData: !Base64 "Fn::Sub": | [settings.ecs] cluster = "${ClusterName}" [settings.ecs.instance-attributes] attribute1 = "foo" attribute2 = "bar" BottlerocketInstanceAutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: !Ref ECSSubnets MixedInstancesPolicy: LaunchTemplate: LaunchTemplateSpecification: LaunchTemplateId: !Ref BottlerocketLaunchTemplate Version: !GetAtt BottlerocketLaunchTemplate.LatestVersionNumber Overrides: - InstanceType: t3.micro - InstanceType: t2.micro InstancesDistribution: OnDemandBaseCapacity: 1 OnDemandPercentageAboveBaseCapacity: 10 SpotInstancePools: 2 MinSize: 0 MaxSize: 5 DesiredCapacity: 0 BottlerocketCapacityProvider: Type: AWS::ECS::CapacityProvider Properties: Name: Bottlerocket-CP AutoScalingGroupProvider: AutoScalingGroupArn: !Ref BottlerocketInstanceAutoScalingGroup ManagedScaling: MaximumScalingStepSize: 3 MinimumScalingStepSize: 1 TargetCapacity: 100 Status: ENABLED ManagedTerminationProtection: DISABLED #--------------------------------- # ECS Cluster #--------------------------------- ECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Ref ClusterName CapacityProviders: - !Ref BottlerocketCapacityProvider ClusterSettings: - Name: containerInsights Value: enabled #--------------------------------- # Internal ALB for ECS #--------------------------------- ALBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub ${ServiceName}-alb-sg Tags: - Key: Name Value: !Sub ${ServiceName}-alb-sg GroupDescription: for Bottlerocket VpcId: !Ref VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref ALBListenerPort ToPort: !Ref ALBListenerPort CidrIp: 0.0.0.0/0 InternalALB: Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" Properties: Name: !Sub "${ClusterName}-alb-internal" Tags: - Key: Name Value: !Sub "${ClusterName}-alb-internal" Scheme: "internal" LoadBalancerAttributes: - Key: "deletion_protection.enabled" Value: false - Key: "idle_timeout.timeout_seconds" Value: 60 - Key: "access_logs.s3.enabled" Value: false SecurityGroups: - !GetAtt ALBSecurityGroup.GroupId Subnets: !Ref ALBSubnets InternalALBTargetGroup: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: VpcId: !Ref VpcId Name: !Sub "${ServiceName}-tg" Protocol: HTTP Port: !Ref ALBListenerPort TargetType: instance InternalALBListener: Type: "AWS::ElasticLoadBalancingV2::Listener" Properties: DefaultActions: - TargetGroupArn: !Ref InternalALBTargetGroup Type: forward LoadBalancerArn: !Ref InternalALB Port: !Ref ALBListenerPort Protocol: HTTP #--------------------------------- # ECS Task #--------------------------------- ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${ClusterName}-ECSTaskExecutionRole" Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy ECSTaskRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${ClusterName}-ECSTaskRole" Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ECSLogGroup: Type: "AWS::Logs::LogGroup" Properties: LogGroupName: !Sub "/ecs/logs/${ClusterName}" ECSTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: nginx-on-bottlerocket Cpu: 256 Memory: 512 NetworkMode: bridge ExecutionRoleArn: !Ref ECSTaskExecutionRole TaskRoleArn: !Ref ECSTaskRole RequiresCompatibilities: - EC2 ContainerDefinitions: - Name: !Ref AppName Image: nginx Essential: true LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref ECSLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: !Ref ClusterName mode: non-blocking max-buffer-size: 100m PortMappings: - ContainerPort: !Ref AppContainerPort HostPort: !Ref HostPort #--------------------------------- # ECS Service #--------------------------------- ECSService: Type: AWS::ECS::Service Properties: CapacityProviderStrategy: - CapacityProvider: !Ref BottlerocketCapacityProvider Weight: 1 DesiredCount: 0 Cluster: !Ref ECSCluster ServiceName: !Ref ServiceName TaskDefinition: !Ref ECSTaskDefinition LoadBalancers: - ContainerName: !Ref AppName ContainerPort: !Ref AppContainerPort TargetGroupArn: !Ref InternalALBTargetGroup Outputs: Endpoint: Value: !Sub "http://${InternalALB.DNSName}"
赤枠部分を入力して実行します。
BottlerocketAmiId
の項目には、上で取得したECS用のBottlerocketのイメージIDを入力します。現時点の ap-northeast-1
の x86_64
の aws-ecs-1
の最新は ami-035653083697abe32
となります。
動かしてみる
初期状態
初期のタスク数を0タスクとしているので、最初は1台もEC2が立ち上がっていない状態です。
オートスケーリング
タスクの設定を変更して必要なタスク数を3に増やしてみます。
暫くするとCloudWatchのAlermが発行されます。
この CapacityProviderReservation
という項目がCapacity Providerから登録された項目で、これによりEC2インスタンス数が変動します。
詳細はこちらに記載があります。
CapacityProviderの「希望するサイズ」が2に増えました。
]
2台のEC2が起動されます。これらのOSは当然 Bottlerocket となります。
ECSのインスタンスとして登録されます。
EC2上で希望した数の3つのタスクが動き出します。
ALB経由で接続してみる
起動したタスクはNGINXでALB配下に紐付けているので、ALB経由で接続可能です。
Cloudformationの出力にALBのURLを出しています。
curl等でアクセスできると思います。
$ curl -sv http://internal-BottlerocketCluster-alb-internal-xxxxxx.ap-northeast-1.elb.amazonaws.com
後はスケールインしたり触ってみて下さい。
Session Managerでログオン
BottlerocketのEC2へのログオンはSession Managerを利用します。
Session Managerの利用は簡単です。
マネジメントコンソールからEC2を選択し、「接続」をクリックします。
セッションマネージャーを選択し、接続します。
以下のようにログオンできます。ただし、通常のLinuxと比較してコマンドもほとんど用意されておらず、できることはかなり少ないです。
OSがAmazon Linux2ベースであることがわかります。
$ cat /etc/os-release NAME="Amazon Linux" VERSION="2" ID="amzn" ID_LIKE="centos rhel fedora" VERSION_ID="2" PRETTY_NAME="Amazon Linux 2" ANSI_COLOR="0;33" CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2" HOME_URL="https://amazonlinux.com/"
ユーザデータで送り込んだ設定の確認
今回Bottlerocket OSの設定としてTOML形式で以下のデータを送り込んでいます。
[settings.ecs] cluster = "ClusterName" [settings.ecs.instance-attributes] attribute1 = "foo" attribute2 = "bar"
settings.ecs.cluster
はクラスタへの接続設定で、実際に接続できています。
settings.ecs.instance-attributes
という設定は以下の通りECSインスタンスの画面から確認できます。
最後に
ここまでがBottlerocket実際に使えるのか確認してみた手順メモになります。
ここに書いてある内容はあくまでdevelopper preview状態のものなので、細かいのはGA後にに使う時に確認してみようと思います。