ECSのタスク定義にライフサイクルを設定したい

#はじめに
ECSのタスクスケジュール機能を利用してバッチ処理などを書いている時、`ecs-cli`を利用して何度もタスクを更新しているとタスク定義の`リビジョン`が増えていきます。

awsのコンソール画面からポチポチしてリビジョンを消す(登録解除)のも面倒ですし、`awscli`の`deregister-task-definition`を利用して消す方法もありますがコマンドラインから実行するのも面倒です。

awscliでの削除の仕方
https://docs.aws.amazon.com/cli/latest/reference/ecs/deregister-task-definition.html

なので、`Lambda`を実行してリビジョンを消す(登録解除)ことにしました。

## 使用したもの
- ServerlessFrameWork lambdaのデプロイ用
- serverless-python-requirements Pythonの外部パッケージをデプロイする用
- Pipenv パッケージ管理

#ECSのタスク定義の一覧を取得する
`boto3`の`ecs.list_task_definitions`を使用してタスク定義を全て取得します。
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.list_task_definitions

引数として`sort='DESC'`にしていますが、これは後の`itertools.groupby`を使用する際に必要なので`'DESC'`に設定しています。


```python

import json
import boto3
import logging

logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)

def get_task_definitions():
    """ECSのタスク定義を取得する

    Returns:
        list -- タスク定義のarnのlist
    """

    client = boto3.client('ecs')
    try:
        res = client.list_task_definitions(
            status='ACTIVE',
            sort='DESC')
        logger.info(json.dumps(res))
    except Exception as e:
        logger.info(e)
    else:
        logger.info('タスク定義取得成功')

    list_task_definitions = res['taskDefinitionArns']
    return list_task_definitions
```

こんな感じに取得できます

```python
[
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_a:3',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_a:2',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_a:1',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_b:5',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_b:4',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_b:3',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_b:2',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_b:1',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_c:5',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_c:4',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_c:3',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_c:2',
    'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXX:task-definition/task_c:1'
]
```

#タスク定義のデータを整形する

タスク定義を取得することができたので同じタスク名同士でグループ化して、タスクがいくつのリビジョンがあるかを確認できるようデータを整形します。
タスク定義を取得しましたが正確にはタスク定義のarnなのでタスク名のみに整形します。

```python

# 整形したい形
{
    'task_a': ['3', '2', '1'],
    'task_b': ['5', '4', '3', '2', '1'],
    'task_c': ['5', '4', '3', '2', '1']
}
```

`itertools.groupby`を使用してグループ化をしていきます。`itertools.groupby`を使用するには事前にソートする必要があるので、`ecs.list_task_definitions`のsortを設定しました。



```python

a = [('a', '3'), ('a', '2'), ('a', '1'), ('b', '4'), ('b', '3'),
     ('b', '2'), ('b', '1'), ('c', '2'), ('c', '1')]

for key, group in groupby(a, lambda x: x[0]): 
    for team in group: 
        print(team)
    print('')
```

こんな感じでグループ化してくれるのでこれを辞書に整形します。

```
('a', '3')
('a', '2')
('a', '1')

('b', '4')
('b', '3')
('b', '2')
('b', '1')

('c', '2')
('c', '1')
```



```python
from itertools import groupby

def groupby_task_definitions(list_task_definitions):
    """タスク定義のarnのlistをまとめる

    Arguments:
        list_task_definitions {list} -- タスク定義のarnのlist

    Returns:
        dict -- タスク名とrevisionのlist

    Example:
        {
            task_a: ['4', '3', '2', '1'],
            task_b: ['2', 1]
        }
    """
    group_list = []
    for task_definition in list_task_definitions:
        # タスク名とrevisionのところのみ切り取る
        task_definition = task_definition.rsplit('/', 1)[1]
        task_name, revision = task_definition.split(':')

        # タプルとしてリストに追加する
        group_list.append((task_name, revision))

    result_dict = {}
    for key, group in groupby(group_list, lambda x: x[0]):
        revision_list = []
        for _, v in group:
            revision_list.append(v)
        result_dict[key] = revision_list

    return result_dict
```


#ECSタスクを消す(登録解除)

タスク定義の辞書を作成することができたのでECSタスクを消していきます。
辞書を`for`で回し、取得した`リビジョン`のリストの長さが指定した数になるまで`pop()`して消していきます。

`ecs.deregister_task_definition`の引数にはタスク定義の名前を与えればいいので楽に実行できます。

```python
def deregister_task_definition(groupby_task_definitions_dict, leave):
    """タスク定義をrevisionが最新のもの2個まで残して削除する

    Arguments:
        groupby_task_definitions_dict {dict} -- タスク名とrevisionのlistを格納したdict
    """
    client = boto3.client('ecs')

    for name, revision_list in groupby_task_definitions_dict.items():
        logger.info(name)
        try:
            while len(revision_list) > 2:
                revision = revision_list.pop()
                client.deregister_task_definition(
                    taskDefinition='{}:{}'.format(name, revision)
                )
        except Exception as e:
            logger.error(e)
        else:
            logger.info('{} : OK!'.format(name))
```


#Lambdaで使えるようにする

現段階ではイベントは設定していませんが`lambda invoke`してもいいですし、`CloudWatchEvents`で定期的に行ってもいいです。


## serverless.yml
`ServerlessFramework`を使用してLambdaを作成するので`serverless.yml`を晒します。

```yml:serverless.yml
service: ecs-task

provider:
  name: aws
  runtime: python3.7
  region: ap-northeast-1
  logRetentionInDays: 30

  iamRoleStatements:
    - Effect: Allow
      Action:
        - ecs:*
      Resource: "*"

functions:
  definition-lifecycle :
    handler: handler.main

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    usePipenv: true 
```

## handler
handlerは今までに行った処理をする関数を実行しているだけです。

```python
def main(event, context):
    list_task_definitions = get_task_definitions()
    groupby_task_definitions_dict = groupby_task_definitions(
        list_task_definitions)
    logger.info(groupby_task_definitions_dict)
    deregister_task_definition(groupby_task_definitions_dict, leave)
```

## コード全体

```python:handler.py

import boto3
import json
from itertools import groupby
import logging

logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)


def get_task_definitions():
    """ECSのタスク定義を取得する

    Returns:
        list -- タスク定義のarnのlist
    """

    client = boto3.client('ecs')
    try:
        res = client.list_task_definitions(
            status='ACTIVE',
            sort='DESC')
        logger.info(json.dumps(res))
    except Exception as e:
        logger.info(e)
    else:
        logger.info('タスク定義取得成功')

    list_task_definitions = res['taskDefinitionArns']
    return list_task_definitions


def groupby_task_definitions(list_task_definitions):
    """タスク定義のarnのlistをまとめる

    Arguments:
        list_task_definitions {list} -- タスク定義のarnのlist

    Returns:
        dict -- タスク名とrevisionのlist

    Example:
        {
            task_a: ['4', '3', '2', '1'],
            task_b: ['2', 1]
        }
    """
    group_list = []
    for task_definition in list_task_definitions:
        # タスク名とrevisionのところのみ切り取る
        task_definition = task_definition.rsplit('/', 1)[1]
        task_name, revision = task_definition.split(':')

        group_list.append((task_name, revision))

    result_dict = {}
    for key, group in groupby(group_list, lambda x: x[0]):
        revision_list = []
        for _, v in list(group):
            revision_list.append(v)
        result_dict[key] = revision_list

    return result_dict


def deregister_task_definition(groupby_task_definitions_dict):
    """タスク定義をrevisionが最新のもの2つまで残して削除する

    Arguments:
        groupby_task_definitions_dict {dict} -- タスク名とrevisionのlistを格納したdict
    """
    client = boto3.client('ecs')

    for name, revision_list in groupby_task_definitions_dict.items():
        logger.info(name)
        try:
            while len(revision_list) > 2:
                revision = revision_list.pop()
                client.deregister_task_definition(
                    taskDefinition='{}:{}'.format(name, revision)
                )
        except Exception as e:
            logger.error(e)
        else:
            logger.info('{} : OK!'.format(name))


def main(event, context):
    list_task_definitions = get_task_definitions()
    groupby_task_definitions_dict = groupby_task_definitions(
        list_task_definitions)
    logger.info(groupby_task_definitions_dict)
    deregister_task_definition(groupby_task_definitions_dict)

```

# AWSなんとかしてくれ
タスク定義の削除(登録解除)は`Active`なタスクのみ行えて、その後は`Inactive`状態になります。その状態になると現時点でが完全に消すことができないのでAWSさん対応を待っています。

>
現時点では、INACTIVE タスク定義はアカウント内で無期限に検出可能な状態で残されます。ただし、この動作は今後変更される場合があるため、関連付けられたタスクとサービスのライフサイクルを超えて保持される INACTIVE タスク定義に依存しないようにしてください。
>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/6288a5c2-d868-1a84-3f1b-d2ab0993ce24.png)



https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/deregister-task-definition.html