[CircleCI]aws ecr put-image実行時に発生したエラーと只管向き合った記録

ECR上にイメージファイルが存在しない状況下で起こったエラーの調査、及びエラーをわかりやすくするための対処と、エラーメッセージ詳細について調べた過程をまとめました。
2020.03.06

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

はじめに

CircleCIでのワークフローでコンテナイメージに対して更新を行う際に、イメージファイルが存在せずにエラーとなった事がありました。都合上例外な手順をとったこと、エラーメッセージが難解だったこともあり、イメージファイルが存在しないことが判明するまでにやや時間を必要としました。

備忘録兼ねて、原因判明までのいきさつ、及び対処と、aws-cli経由で表示される独特のエラーメッセージについて書いてみました。

発生していた状況について

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

#!/bin/bash -eo pipefail
MANIFEST=$(aws ecr batch-get-image --repository-name repos --image-ids imageTag=master_${CIRCLE_SHA1:0:7} --query images[].imageManifest --output text)

aws ecr put-image --repository-name repos --image-tag prod_${CIRCLE_TAG} --image-manifest "$MANIFEST"
Parameter validation failed: Invalid length for parameter imageManifest, value: 0, valid range: 1-inf

Exited with code exit status 255

imageManifestパラメータのエラーとありますが、value: 0, valid range: 1-infがよくわかりません。とりあえずはaws ecr put-imageに渡されている--image-manifestに問題があると想定し、$MANIFESTをみてみました。CIRCLE_SHA1については Preparing Environment Variables のログを見ることで確認できます。

aws ecr batch-get-image \
  --repository-name repos \
  --image-ids imageTag=master_${CIRCLE_SHA1:0:7} \
  --query 'images[].imageManifest' \
  --output text

結果、何も返ってきませんでした。この時点でイメージが存在していない可能性に気が付きます。

エラーをわかりやすくしてみる

何も分からない状態が早期解消できた感もありますが、どうせなら迷うこと無く直ぐに原因が判るようにしておきたいものです。

when: on_failを併用して、以下のようなフローを試してみました。

jobs:
  image_exist_check:
    docker:
      - image: circleci/python:3.7.3
        environment:
          PIPENV_VENV_IN_PROJECT: true
    resource_class: small
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - aws-cli/setup
      - restore_cache:
          key: pipenv-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
      - run:
          command: |
            pipenv sync --dev
            exit 1
      - run:
          name: run tests
          when: on_fail
          command: |
            result=$(aws ecr describe-images --repository-name airflow | jq -r '.imageDetails[].imageTags[] | contains("XXXXXXXXX")' | grep true)
            if [ "$result" != "true" ]; then
                echo 'Image not found'
            fi
workflows:
  version: 2 
  cicd_pipeline:
    jobs:
      - image_exist_check:
          filters:
            tags:
              ignore: /.*/
            branches:
              only: image_exist_check

pipenv sync後に強制終了させています。exit 1にてその後のon_failにて呼ばれて、指定のイメージタグがなければメッセージが残る仕組みです。タグは常にユニークとなるため、trueが2つ以上渡ることはありません。

以下のコマンドによる判定で有無をチェックしています。

result=$(aws ecr describe-images --repository-name airflow | jq -r '.imageDetails[].imageTags[] | contains("XXXXXXXXX")' | grep true)

参考までに、aws ecr describe-imagesにてタグが付いているイメージが多く存在する場合は以下のようなレスポンスになります。

% aws ecr describe-images --repository-name airflow | jq -r '.'
{
  "imageDetails": [
    {
      "imageSizeInBytes": 636296435,
      "imageDigest": "sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "imageTags": [
        "dev_00000001"
      ],
      "registryId": "000000000001",
      "repositoryName": "repos",
      "imagePushedAt": 1581502156
    },
    {
      "imageSizeInBytes": 673563332,
      "imageDigest": "sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "imageTags": [
        "prod_v1.0.0",
        "master_00000001"
      ],
      "registryId": "000000000000",
      "repositoryName": "repos",
      "imagePushedAt": 1572410914
    },
    ..
  ]
}

value: 0, valid range: 1-inf の解

と、上記の流れで原因と対処方法、今後の対処に手間も取らなくなりましたが、CircleCIログ上に残っていたvalue: 0, valid range: 1-infが解せません。aws-cli絡みであるとだけ予想はできるので、試しに辿ってみることにしました。

aws-cliのソースからたどり、更にboto3を辿り、更にbotocoreまで辿って見つけたのが以下のコードです。

def range_check(name, value, shape, error_type, errors):
    failed = False
    min_allowed = float('-inf')
    max_allowed = float('inf')
    if 'min' in shape.metadata:
        min_allowed = shape.metadata['min']
        if value < min_allowed:
            failed = True
    elif hasattr(shape, 'serialization'):
        # Members that can be bound to the host have an implicit min of 1
        if shape.serialization.get('hostLabel'):
            min_allowed = 1
            if value < min_allowed:
                failed = True
    if failed:
        errors.report(name, error_type, param=value,
                      valid_range=[min_allowed, max_allowed])

botocore/validate.py at 8a9b18a3632120181eb186ad0685c208160a4a6a · boto/botocore

Pythonでfloat('inf')と記述すると、無限大を表すinfが生成されます。float('-inf')は負の無限大です。

% python
>> float('inf')
inf
>> float('-inf')
-inf</p>

大文字か小文字かは関係なく、例えば "inf"、 "Inf"、 "INFINITY" 、 "iNfINity" は全て正の無限大として使える綴りです。

組み込み関数 — Python 3.8.2 ドキュメント

更にエラーメッセージを実際に返している関数も見てみます。

        elif error_type == 'invalid length':
            min_allowed = additional['valid_range'][0]
            max_allowed = additional['valid_range'][1]
            return ('Invalid length for parameter %s, value: %s, valid range: '
                    '%s-%s' % (name, additional['param'],
                               min_allowed, max_allowed))

botocore/validate.py at 8a9b18a3632120181eb186ad0685c208160a4a6a · boto/botocore

エラーメッセージは1-infとなっているため、min_allowedが1であり、max_allowedがinfとなるわけです。valueが0だったことを踏まえると、何も渡ってきてないから何か入れろってことですね。

あとがき

手順が例外だったことと、botocore独特のエラーメッセージも独特だったために手間取ってしまいましたが、botocore自体はaws-cliを使う上で避けられないライブラリでもあるため、エラーを確認する際の事前知識を知るいい機会となりました。

CICDのワークフローにaws-cliを利用されている場合に、エラーの対処をどうするべきか迷った際にはbotocoreまで掘り下げてみることもおすすめします。