BottlerocketをECSのAutoScalingに組み込むCloudformation定義と手順

AWSのコンテナ専用OSである Bottlerocket をECSクラスタに付けてオートスケールしてみました。

この記事ではBottlerocket OSの aws-ecs-1 というバリアントを利用しますが、このバリアントは developer preview フェーズなので、今後変わる可能性があります。

Bottlerocketとは

この記事にたどり着く人に伝えるような内容では無いと思うのでAWSのブログのリンク貼っておきます。

aws.amazon.com

実際に見ることが多いのは以下のGithubリポジトリとなります。

github.com

ECSでBottlerocketを使うために

Bottlerocketのバリアント

Bottlerocketにはバリアント(Variants)というものがあります。

例えば aws-k8s-1.19aws-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-proxynetwork.no-proxy みたいな項目がEKS側は対応されているようです。ECS側のバリアントでは現時点では動かなかったですが将来的に対応されるのではと思います。

何ができて、何ができないか(開発中か)、AWSのコンテナ関連はロードマップが公開されてますが、Bottlerocketについては以下を見る方が良いかもしれません。

Issues · bottlerocket-os/bottlerocket · GitHub

Cloudformationのデプロイ

InternalのALB配下でNGINXを動かす以下の基本構成を調整して作ってみます。

yomon.hatenablog.com

以下の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-1x86_64aws-ecs-1 の最新は ami-035653083697abe32 となります。

f:id:yomon8:20210330122454p:plain:w500

動かしてみる

初期状態

初期のタスク数を0タスクとしているので、最初は1台もEC2が立ち上がっていない状態です。

f:id:yomon8:20210330123541p:plain:w500

f:id:yomon8:20210330123637p:plain:w500

オートスケーリング

タスクの設定を変更して必要なタスク数を3に増やしてみます。

f:id:yomon8:20210330123754p:plain:w500

暫くするとCloudWatchのAlermが発行されます。

f:id:yomon8:20210330123905p:plain:w300

この CapacityProviderReservation という項目がCapacity Providerから登録された項目で、これによりEC2インスタンス数が変動します。

詳細はこちらに記載があります。

aws.amazon.com

CapacityProviderの「希望するサイズ」が2に増えました。

f:id:yomon8:20210330124053p:plain:w500]

2台のEC2が起動されます。これらのOSは当然 Bottlerocket となります。

f:id:yomon8:20210330121112p:plain:w500

ECSのインスタンスとして登録されます。

f:id:yomon8:20210330121046p:plain:w500

EC2上で希望した数の3つのタスクが動き出します。

f:id:yomon8:20210330124310p:plain:w500

ALB経由で接続してみる

起動したタスクはNGINXでALB配下に紐付けているので、ALB経由で接続可能です。

Cloudformationの出力にALBのURLを出しています。

f:id:yomon8:20210330121632p:plain

curl等でアクセスできると思います。

$ curl -sv http://internal-BottlerocketCluster-alb-internal-xxxxxx.ap-northeast-1.elb.amazonaws.com 

後はスケールインしたり触ってみて下さい。

Session Managerでログオン

BottlerocketのEC2へのログオンはSession Managerを利用します。

Session Managerの利用は簡単です。

マネジメントコンソールからEC2を選択し、「接続」をクリックします。

f:id:yomon8:20210330124649p:plain:w400

セッションマネージャーを選択し、接続します。

f:id:yomon8:20210330124722p:plain:w400

以下のようにログオンできます。ただし、通常のLinuxと比較してコマンドもほとんど用意されておらず、できることはかなり少ないです。

f:id:yomon8:20210330124749p:plain:w400

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インスタンスの画面から確認できます。

f:id:yomon8:20210330125128p:plain

最後に

ここまでがBottlerocket実際に使えるのか確認してみた手順メモになります。

ここに書いてある内容はあくまでdevelopper preview状態のものなので、細かいのはGA後にに使う時に確認してみようと思います。