ECS Execの有効化が原因でECSタスクがPROVISIONING状態から遷移しなくなった話

よくあるPENDING状態で固まるやつかと思ったらどハマリしてしまいました
2022.10.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。

先日ある環境でECSのタスクがPROVISIONING状態のまま固まって(PENDINGではないのがポイントです!)起動できなくなるという問題が発生しました。※正確にいうとPROVISIONING状態で30分程度固まる→STOPPEDに遷移→新しいタスクがPROVISIONINGされ再度30分程度固まり...という状況です。

最初はNATゲートウェイ周りの設定漏れやECRのVPCエンドポイント設定漏れに起因したよくある問題かと思っていたのですが、色々調査しても一向に原因が分かりませんでした。最終的にECS Execの有効化が原因だったことが分かったので、同じ事象でハマる人が出ないように改めてブログにまとめてみました。

結論

先に結論ですがECS Execを有効化したECSサービスでAuto Scaling グループキャパシティープロバイダーを利用するとタスクが起動できなくなるようです。ドキュメントに記載されている以下の制限事項に抵触するためと思われます。

ECS Exec は現在、Auto Scaling グループのキャパシティープロバイダーを使用して起動されたタスクに対応していません。

ECS Exec を使用するための考慮事項

環境など

問題が発生していた環境はざっくり以下のような環境でした

  • ECS on EC2を利用
  • ネットワークモードはdefault(bridge)
  • 特にタスク配置戦略やタスク配置の制限は設定されていない
  • ASG配下のEC2インスタンスが5台起動していて、クラスタ全体で見るとCPUやメモリは余裕がある状態
  • 3つのECSサービスが存在する ※ECSサービスA~Cとします。
    • これらのECSサービスは全て同一のコンテナイメージを利用する

事象

この環境下で、ある時点からECSサービスAのタスクが起動できなくなりました。ただし、この状態でもECSサービスB,Cに関してはタスク数を増やすと普通にタスクが起動できる状態です。状況を詳しく確認するとECSタスクがPROVISIONINGのまま一向に進まない状態です。

Stopped状態のタスクのログを確認しても何もログが出力されていません。

ECSサービスのイベントを見てもservice <サービス名> has started 1 tasks: task <タスクID>.というログが続くだけで、特にエラーログのようなものは出力されていませんでした。

AWSのナレッジセンターの記述を参考に1つ1つ原因候補を潰していきましたが、特に原因らしきものは見つかりませんでした。

Amazon ECS タスクが保留状態のまま止まっているのはなぜですか?

その後色々あってサービスAにECS Execを有効化する更新が入っていたことが判明したため、改めてECS Execの無効化を試行すると無事にタスクが起動できるようになりました。その後ECS Execに関するドキュメントを確認すると結論のセクションで記載した通りAuto Scaling グループキャパシティープロバイダーはECS Execに非対応という記述が見つかりました。

検証してみる

ECS Execの有効化が原因のようだと切り分けられたので、改めて検証してみます。

まず以下のCFnテンプレートを使ってECS関連のリソースをまとめてデプロイします。これはCloudFormationのドキュメント記載のAmazon Elastic Container Service テンプレートスニペット ほぼそのままです。ECS Execに対応した環境を利用したいので、AMI IDの部分だけ本日時点での最新版ami-0ed1560bb509908b1に変更しています。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  KeyName:
    Type: 'AWS::EC2::KeyPair::KeyName'
    Description: Name of an existing EC2 KeyPair to enable SSH access to the ECS instances.
  VpcId:
    Type: 'AWS::EC2::VPC::Id'
    Description: Select a VPC that allows instances to access the Internet.
  SubnetId:
    Type: 'List<AWS::EC2::Subnet::Id>'
    Description: Select at least two subnets in your selected VPC.
  DesiredCapacity:
    Type: Number
    Default: '1'
    Description: Number of instances to launch in your ECS cluster.
  MaxSize:
    Type: Number
    Default: '1'
    Description: Maximum number of instances that can be launched in your ECS cluster.
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t2.micro
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.medium
      - t2.large
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - c4.large
      - c4.xlarge
      - c4.2xlarge
      - c4.4xlarge
      - c4.8xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
    ConstraintDescription: Please choose a valid instance type.
Mappings:
  AWSRegionToAMI:
    us-east-1:
      AMIID: ami-09bee01cc997a78a6
    us-east-2:
      AMIID: ami-0a9e12068cb98a01d
    us-west-1:
      AMIID: ami-0fa6c8d131a220017
    us-west-2:
      AMIID: ami-078c97cf1cefd1b38
    eu-west-1:
      AMIID: ami-0c9ef930279337028
    eu-central-1:
      AMIID: ami-065c1e34da68f2b02
    ap-northeast-1:
      AMIID: ami-0ed1560bb509908b1 
      #AMIID: ami-02265963d1614d04d
    ap-southeast-1:
      AMIID: ami-0b68661b29b9e058c
    ap-southeast-2:
      AMIID: ami-00e4b147599c13588
Resources:
  ECSCluster:
    Type: 'AWS::ECS::Cluster'
  EcsSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: ECS Security Group
      VpcId: !Ref VpcId
  EcsSecurityGroupHTTPinbound:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      GroupId: !Ref EcsSecurityGroup
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      CidrIp: 0.0.0.0/0
  EcsSecurityGroupSSHinbound:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      GroupId: !Ref EcsSecurityGroup
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22
      CidrIp: 0.0.0.0/0
  EcsSecurityGroupALBports:
    Type: 'AWS::EC2::SecurityGroupIngress'
    Properties:
      GroupId: !Ref EcsSecurityGroup
      IpProtocol: tcp
      FromPort: 31000
      ToPort: 61000
      SourceSecurityGroupId: !Ref EcsSecurityGroup
  CloudwatchLogsGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Join 
        - '-'
        - - ECSLogGroup
          - !Ref 'AWS::StackName'
      RetentionInDays: 14
  taskdefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      Family: !Join 
        - ''
        - - !Ref 'AWS::StackName'
          - '-ecs-demo-app'
      ContainerDefinitions:
        - Name: simple-app
          Cpu: '10'
          Essential: 'true'
          Image: 'httpd:2.4'
          Memory: '300'
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref CloudwatchLogsGroup
              awslogs-region: !Ref 'AWS::Region'
              awslogs-stream-prefix: ecs-demo-app
          MountPoints:
            - ContainerPath: /usr/local/apache2/htdocs
              SourceVolume: my-vol
          PortMappings:
            - ContainerPort: 80
        - Name: busybox
          Cpu: 10
          Command:
            - >-
              /bin/sh -c "while true; do echo '>html< >head< >title<Amazon ECS
              Sample App>/title< >style<body {margin-top: 40px;
              background-color: #333;} >/style< >/head<>body< >div
              style=color:white;text-align:center< >h1<Amazon ECS Sample
              App>/h1< >h2<Congratulations!>/h2< >p<Your application is now
              running on a container in Amazon ECS.>/p<' < top; /bin/date < date
              ; echo '>/div<>/body<>/html<' < bottom; cat top date bottom <
              /usr/local/apache2/htdocs/index.html ; sleep 1; done"
          EntryPoint:
            - sh
            - '-c'
          Essential: false
          Image: busybox
          Memory: 200
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref CloudwatchLogsGroup
              awslogs-region: !Ref 'AWS::Region'
              awslogs-stream-prefix: ecs-demo-app
          VolumesFrom:
            - SourceContainer: simple-app
      Volumes:
        - Name: my-vol
  ECSALB:
    Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
    Properties:
      Name: ECSALB
      Scheme: internet-facing
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: '30'
      Subnets: !Ref SubnetId
      SecurityGroups:
        - !Ref EcsSecurityGroup
  ALBListener:
    Type: 'AWS::ElasticLoadBalancingV2::Listener'
    DependsOn: ECSServiceRole
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref ECSTG
      LoadBalancerArn: !Ref ECSALB
      Port: '80'
      Protocol: HTTP
  ECSALBListenerRule:
    Type: 'AWS::ElasticLoadBalancingV2::ListenerRule'
    DependsOn: ALBListener
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref ECSTG
      Conditions:
        - Field: path-pattern
          Values:
            - /
      ListenerArn: !Ref ALBListener
      Priority: 1
  ECSTG:
    Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
    DependsOn: ECSALB
    Properties:
      HealthCheckIntervalSeconds: 10
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      Name: ECSTG
      Port: 80
      Protocol: HTTP
      UnhealthyThresholdCount: 2
      VpcId: !Ref VpcId
  ECSAutoScalingGroup:
    Type: 'AWS::AutoScaling::AutoScalingGroup'
    Properties:
      VPCZoneIdentifier: !Ref SubnetId
      LaunchConfigurationName: !Ref ContainerInstances
      MinSize: '1'
      MaxSize: !Ref MaxSize
      DesiredCapacity: !Ref DesiredCapacity
    CreationPolicy:
      ResourceSignal:
        Timeout: PT15M
    UpdatePolicy:
      AutoScalingReplacingUpdate:
        WillReplace: 'true'
  ContainerInstances:
    Type: 'AWS::AutoScaling::LaunchConfiguration'
    Properties:
      ImageId: !FindInMap 
        - AWSRegionToAMI
        - !Ref 'AWS::Region'
        - AMIID
      SecurityGroups:
        - !Ref EcsSecurityGroup
      InstanceType: !Ref InstanceType
      IamInstanceProfile: !Ref EC2InstanceProfile
      KeyName: !Ref KeyName
      UserData: !Base64 
        'Fn::Join':
          - ''
          - - |
              #!/bin/bash -xe
            - echo ECS_CLUSTER=
            - !Ref ECSCluster
            - |2
               >> /etc/ecs/ecs.config
            - |
              yum install -y aws-cfn-bootstrap
            - '/opt/aws/bin/cfn-signal -e $? '
            - '         --stack '
            - !Ref 'AWS::StackName'
            - '         --resource ECSAutoScalingGroup '
            - '         --region '
            - !Ref 'AWS::Region'
            - |+

  service:
    Type: 'AWS::ECS::Service'
    DependsOn: ALBListener
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: '1'
      LoadBalancers:
        - ContainerName: simple-app
          ContainerPort: '80'
          TargetGroupArn: !Ref ECSTG
      Role: !Ref ECSServiceRole
      TaskDefinition: !Ref taskdefinition
  ECSServiceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: ecs-service
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer'
                  - 'elasticloadbalancing:DeregisterTargets'
                  - 'elasticloadbalancing:Describe*'
                  - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer'
                  - 'elasticloadbalancing:RegisterTargets'
                  - 'ec2:Describe*'
                  - 'ec2:AuthorizeSecurityGroupIngress'
                Resource: '*'
  ServiceScalingTarget:
    Type: 'AWS::ApplicationAutoScaling::ScalableTarget'
    DependsOn: service
    Properties:
      MaxCapacity: 2
      MinCapacity: 1
      ResourceId: !Join 
        - ''
        - - service/
          - !Ref ECSCluster
          - /
          - !GetAtt 
            - service
            - Name
      RoleARN: !GetAtt 
        - AutoscalingRole
        - Arn
      ScalableDimension: 'ecs:service:DesiredCount'
      ServiceNamespace: ecs
  ServiceScalingPolicy:
    Type: 'AWS::ApplicationAutoScaling::ScalingPolicy'
    Properties:
      PolicyName: AStepPolicy
      PolicyType: StepScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: PercentChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - MetricIntervalLowerBound: 0
            ScalingAdjustment: 200
  ALB500sAlarmScaleUp:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      EvaluationPeriods: '1'
      Statistic: Average
      Threshold: '10'
      AlarmDescription: Alarm if our ALB generates too many HTTP 500s.
      Period: '60'
      AlarmActions:
        - !Ref ServiceScalingPolicy
      Namespace: AWS/ApplicationELB
      Dimensions:
        - Name: LoadBalancer
          Value: !GetAtt 
            - ECSALB
            - LoadBalancerFullName
      ComparisonOperator: GreaterThanThreshold
      MetricName: HTTPCode_ELB_5XX_Count
  EC2Role:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: ecs-service
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - 'ecs:CreateCluster'
                  - 'ecs:DeregisterContainerInstance'
                  - 'ecs:DiscoverPollEndpoint'
                  - 'ecs:Poll'
                  - 'ecs:RegisterContainerInstance'
                  - 'ecs:StartTelemetrySession'
                  - 'ecs:Submit*'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: '*'
  AutoscalingRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - application-autoscaling.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: service-autoscaling
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - 'application-autoscaling:*'
                  - 'cloudwatch:DescribeAlarms'
                  - 'cloudwatch:PutMetricAlarm'
                  - 'ecs:DescribeServices'
                  - 'ecs:UpdateService'
                Resource: '*'
  EC2InstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      Roles:
        - !Ref EC2Role
Outputs:
  ecsservice:
    Value: !Ref service
  ecscluster:
    Value: !Ref ECSCluster
  ECSALB:
    Description: Your ALB DNS URL
    Value: !Join 
      - ''
      - - !GetAtt 
          - ECSALB
          - DNSName
  taskdef:
    Value: !Ref taskdefinition

このテンプレートでデプロイしたECSサービスはECS Execが有効化されていないため、AWS CLIからECS Execを有効化しておきます。

aws ecs update-service --cluster クラスタ名 --service サービス名 --enable-execute-command

この時点ではECSサービスの起動タイプはEC2に設定されています。

このままタスクの数を1にしてみましょう。

無事に起動できました。起動タイプ:EC2の状態であればECS Execが有効でもタスクが起動できることが確認できました。一旦タスクの数を0に戻しておきます。

今度は起動タイプ:EC2となっていたところをキャパシティープロバイダー戦略に切り替えてみましょう。キャパシィプロバイダーの定義を作成した後、サービスの設定を更新します。

設定が更新できたら再度タスクの数を1に更新します。すると...タスクがPROVISIONING状態の進まない事象が再現できました

各種ログを確認してみる

無事に?タスクが起動しない状態が再現できましたが、意図せずこの状況を引き起こした場合に気づく術はないのでしょうか?諸々のログを見ていきたいと思います。まずECSタスクとECSサービスのイベントからは前述の通り有用な情報が得られませんでした。

ECSエージェントのログはどうでしょうか?ログレベルをdebugに上げた状態でEC2インスタンスの/var/log/ecs/ecs-agent.logを確認してみます。

level=debug time=2022-10-19T11:43:39Z msg="No container health metrics to report" module=client.go
level=debug time=2022-10-19T11:43:39Z msg="Received message of type: AckPublishInstanceStatus" module=client.go
level=debug time=2022-10-19T11:43:39Z msg="Received AckPublishInstanceStatus from tcs" module=handler.go
level=debug time=2022-10-19T11:43:56Z msg="Instance is idle. No task metrics to report" module=engine.go
level=debug time=2022-10-19T11:43:56Z msg="Received message of type: AckPublishMetric" module=client.go
level=debug time=2022-10-19T11:43:56Z msg="Received AckPublishMetric from tcs" module=handler.go
level=debug time=2022-10-19T11:43:59Z msg="Skipping publishing container instance status message that was already sent" module=client.go
level=debug time=2022-10-19T11:43:59Z msg="No container health metrics to report" module=client.go
level=debug time=2022-10-19T11:44:01Z msg="Received message of type: HeartbeatMessage" module=client.go
level=debug time=2022-10-19T11:44:01Z msg="ACS activity occurred" module=acs_handler.go
level=debug time=2022-10-19T11:44:01Z msg="instance healthcheck result: OK" module=doctor.go
level=debug time=2022-10-19T11:44:02Z msg="Handling http requestmethodHEADfrom127.0.0.1:45496" module=logging_handler.go
level=debug time=2022-10-19T11:44:16Z msg="Instance is idle. No task metrics to report" module=engine.go
level=debug time=2022-10-19T11:44:16Z msg="Received message of type: AckPublishMetric" module=client.go
level=debug time=2022-10-19T11:44:16Z msg="Received AckPublishMetric from tcs" module=handler.go
level=debug time=2022-10-19T11:44:19Z msg="No container health metrics to report" module=client.go
level=debug time=2022-10-19T11:44:19Z msg="Received message of type: AckPublishInstanceStatus" module=client.go
level=debug time=2022-10-19T11:44:19Z msg="Received AckPublishInstanceStatus from tcs" module=handler.go
level=debug time=2022-10-19T11:44:26Z msg="Received message of type: HeartbeatMessage" module=client.go
level=debug time=2022-10-19T11:44:26Z msg="ACS activity occurred" module=acs_handler.go
level=debug time=2022-10-19T11:44:26Z msg="instance healthcheck result: OK" module=doctor.go
level=debug time=2022-10-19T11:44:32Z msg="Handling http requestmethodHEADfrom127.0.0.1:53362" module=logging_handler.go
level=debug time=2022-10-19T11:44:36Z msg="Instance is idle. No task metrics to report" module=engine.go
level=debug time=2022-10-19T11:44:36Z msg="Received message of type: AckPublishMetric" module=client.go
level=debug time=2022-10-19T11:44:36Z msg="Received AckPublishMetric from tcs" module=handler.go
level=debug time=2022-10-19T11:44:39Z msg="No container health metrics to report" module=client.go
level=debug time=2022-10-19T11:44:39Z msg="Received message of type: AckPublishInstanceStatus" module=client.go
level=debug time=2022-10-19T11:44:39Z msg="Received AckPublishInstanceStatus from tcs" module=handler.go
level=debug time=2022-10-19T11:44:51Z msg="Received message of type: HeartbeatMessage" module=client.go
level=debug time=2022-10-19T11:44:51Z msg="ACS activity occurred" module=acs_handler.go
level=debug time=2022-10-19T11:44:51Z msg="instance healthcheck result: OK" module=doctor.go
level=debug time=2022-10-19T11:44:56Z msg="Instance is idle. No task metrics to report" module=engine.go
level=debug time=2022-10-19T11:44:56Z msg="Received message of type: AckPublishMetric" module=client.go
level=debug time=2022-10-19T11:44:56Z msg="Received AckPublishMetric from tcs" module=handler.go
level=debug time=2022-10-19T11:44:59Z msg="No container health metrics to report" module=client.go
level=debug time=2022-10-19T11:44:59Z msg="Received message of type: AckPublishInstanceStatus" module=client.go
...略

特にエラーらしくログは出力されていません。

Event Bridgeに送信されるECSイベントから何か分かったりしないでしょうか?こちらのブログで紹介しているようなやり方でECSイベントをEvent Bridge経由でCW Logsに保管し、ログを確認してみました。

ECS タスクの停止理由を CloudWatch Logs に保管する方法を教えてください

以下のようなログが出力されていました。

{
    "version": "0",
    "id": "ac39557e-a880-4a00-cd3d-a6694581132b",
    "detail-type": "ECS Task State Change",
    "source": "aws.ecs",
    "account": "<AWSアカウントID>",
    "time": "2022-10-19T02:27:38Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ecs:ap-northeast-1:<AWSアカウントID>:task/<クラスタ名>/428df540a5e2420a9c5b207ba465e1c9"
    ],
    "detail": {
        "attachments": [],
        "capacityProviderName": "<キャパシティプロバイダー名>",
        "clusterArn": "<ECSクラスタのARN>",
        "containers": [],
        "cpu": "20",
        "createdAt": "2022-10-19T01:57:36.165Z",
        "desiredStatus": "STOPPED",
        "enableExecuteCommand": false,
        "group": "service:<ECSサービス名>",
        "launchType": "EC2",
        "lastStatus": "STOPPED",
        "memory": "500",
        "overrides": {
            "containerOverrides": []
        },
        "startedBy": "ecs-svc/7648301721650541975",
        "stoppingAt": "2022-10-19T02:27:38.829Z",
        "stoppedAt": "2022-10-19T02:27:38.829Z",
        "stoppedReason": "Task failed to start",
        "stopCode": "TaskFailedToStart",
        "taskArn": "<ECSタスクのARN>",
        "taskDefinitionArn": "<ECSタスク定義のARN>",
        "updatedAt": "2022-10-19T02:27:38.829Z",
        "version": 2
    }
}

stoppedReasonに"Task failed to start"と出力されていますが、これ以上詳細な情報は分からなそうです。残念。

まとめ

ECSタスクがPENDING状態で固まるという状況は何度か経験しており、どうせ類似の事象だろうと高をくくっていたのですが、今回経験した事象はPROVISIONING状態で固まってしまうという事象でした。各種ログを見ても有用な情報は見当たりませんし、ECS Execの有効/無効はマネコンからは判断できないことから、トラブルシューティングとしてはなかなか難易度の高い事象だったのではないでしょうか。同様の事象でハマった方の参考になれば幸いです。

参考