一時クレデンシャルを取得するserverlessコマンドのラッパースクリプト

EC2にアタッチされたIAM Roleからassume roleして、一時クレデンシャルを取得して、Serverless Frameworkを実行するスクリプトを書きました。

serverlessコマンドのラッパーとして機能する簡易的なスクリプトです。

--aws-profile が指定されていた場合に ~/.aws/config を読み込み、 credential_source=EC2InstanceMetadata かつ role_arn が指定されていた場合は、一時クレデンシャルを取得してserverlessコマンドを実行します。それ以外の場合は、なにも加工せずにそのままserverlessを実行します。

スクリプト

シェルスクリプトですが、Pythonコードも埋め込まれています。

#!/bin/bash

# serverlessコマンドのラッパーとして機能する簡易的なスクリプト。
#
# 最初のオプションで --aws-profile が指定されていた場合に
# ~/.aws/config を読み込み、
# credential_source=EC2InstanceMetadata かつ role_arn が指定されていた場合は、
# 一時クレデンシャルを取得してserverlessを実行する。
# それ以外の場合は、なにもせずにserverlessを実行する。

profile=
if [ $# -ge 2 ]; then
    if [ "$1" = --aws-profile ]; then
        profile="$2"
    fi
fi

if [ -z "$profile" ]; then
    # そのままserverlessを実行
    echo serverless "$@"
    exec serverless "$@"
fi

################################
# .aws/config から role_arn を取得
################################

role_arn=$(python <<EOF
import configparser
import os

home_path = "$HOME"

aws_config_filepath = home_path + "/.aws/config"

profile = "$profile"

def fetch_aws_config_role_arn(profile):
    if not os.path.exists(aws_config_filepath):
        return None

    aws_config = configparser.ConfigParser()
    aws_config.read(aws_config_filepath)

    section_name = "profile " + profile
    try:
        credential_source = aws_config.get(section_name, "credential_source")
    except configparser.NoOptionError:
        return None
    except configparser.NoSectionError:
        return None
    if credential_source != "Ec2InstanceMetadata":
        return None
    try:
        role_arn = aws_config.get(section_name, "role_arn")
    except configparser.NoOptionError:
        return None
    return role_arn

role_arn = fetch_aws_config_role_arn(profile)
if role_arn != None:
    print(role_arn)
EOF
)

################################

if [ -n "$role_arn" ]; then # role_arn が取得できた場合
    # 一時クレデンシャルを取得
    echo aws sts assume-role --role-arn $role_arn --role-session-name serverless-deploy
    json=$(aws sts assume-role --role-arn $role_arn --role-session-name serverless-deploy)
    export AWS_ACCESS_KEY_ID=$(echo $json | jq ".Credentials.AccessKeyId" -r)
    export AWS_SECRET_ACCESS_KEY=$(echo $json | jq ".Credentials.SecretAccessKey" -r)
    export AWS_SESSION_TOKEN=$(echo $json | jq ".Credentials.SessionToken" -r)

    # --aws-profile の指定を削除してserverlessを実行
    shift
    shift
    echo serverless "$@"
    exec serverless "$@"
fi

# そのままserverlessを実行
echo serverless "$@"
exec serverless "$@"

################################

serverless という名前で実行権限を付けて、PATHの通るところに置けば、普通のserverlessコマンドのように動かせます。

リンク

このスクリプトに至る経緯

数式のある記事をKibelaのフォーマットからはてなブログのフォーマットに変換

約1年前にはてなブログを始めた時に、数式の書き方がわからなくて試行錯誤していました。(このときの記事→ はてなブログでのMarkdownでの数式の書き方 その2

私は下書きにKibelaのサービスを使っています。Kibelaは数式を書くのも比較的わかりやすいです。

そこで、Kibelaのフォーマットからはてなブログのフォーマットに変換する、簡易的なスクリプトを書きました。

  • インラインの数式は $$$$ で囲む
  • ブロックの数式は ```math``` で囲む

  • インラインの数式は [tex: \displaystyle] で囲む
  • ブロックの数式は <pre class="math">[tex: \displaystyle]</pre> で囲む

に変換します。

たまにエスケープ処理で変換に失敗しているときがありますが、以下のスクリプトを実用的に使っています。

HTML/JavaScriptのコード

<!DOCTYPE html>
<html>
<head>
<title>kibela-to-hatena</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

<script>

$(function(){
  $("#src").focus(function() {
    $("#src").select();
  });
  $("#dst").focus(function() {
    $("#dst").select();
  });
  $("#convertBtn").click(function() {
    var str = $("#src").val();

    function replaceInline(match, p1, off, str) {
      p1 = p1.replace(/\^/g, "\\^");
      p1 = p1.replace(/_/g, "\\_");
      return "[tex: \\displaystyle " + p1 + "]";
    }

    function replaceBlock(match, p1, off, str) {
      p1 = p1.replace(/\]/g, "\\]");
      return "<pre class=\"math\">[tex: \\displaystyle\n" + p1 + "\n]</pre>\n";
    }

    str = str.replace(/\$\$(.+?)\$\$/g, replaceInline);
    str = str.replace(/(?<=\n)```math\n(.+?)\n```\n/sg, replaceBlock);
    str = str.replace(/(?<=\n)```([a-z]*)\n(.+?)\n```\n/sg, "<pre class=\"$1\">\n$2\n</pre>\n");

    $("#dst").val(str);
  });
});

</script>
</head>
<body>

<div>
  <textarea id="src"></textarea>
</div>

<div>
  <input id="convertBtn" type="button" value="↓変換">
</div>

<div>
  <textarea id="dst"></textarea>
</div>

</body>
</html>

このHTMLを適当なブラウザで開くと使えます。

以下のURLで公開しました。

https://suzuki-navi.github.io/suzuki-navi-converter/

サンプル

Kibelaでのソース

 インライン $$ e^{i\pi} = -1 $$

 ブロック

 ```math
 \begin{align}
 e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} = -1 \\
 x &= \sigma
 \end{align}
 ```

はてなブログでのソース

 インライン [tex: \displaystyle  e\^{i\pi} = -1 ]

 ブロック

 <pre class="math">[tex: \displaystyle
 \begin{align}
 e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} = -1 \\
 x &= \sigma
 \end{align}
 ]</pre>

はてなブログでの見た目

インライン  \displaystyle  e^{i\pi} = -1

ブロック

 \displaystyle
\begin{align}
e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} = -1 \\
x &= \sigma
\end{align}

チェビシェフ多項式(Chebyshev polynomials)でフィットさせた係数から値を復元

区間  \displaystyle  [t_s, t_e ] の  \displaystyle  t に対して、  \displaystyle  f(t) をチェビシェフ多項式でフィットさせた係数  \displaystyle  c_0, c_1, c_2, \cdots, c_N が与えられているとします。

係数から、以下の手順で  \displaystyle  f(t) を計算できます。

手順

 \displaystyle  t 区間  \displaystyle  [-1, 1] に正規化したものを  \displaystyle  x とします。

 \displaystyle
x = 2\frac{t - t_s}{t_e - t_s} - 1

チェビシェフ多項式(Chebyshev polynomials)  \displaystyle  T_0, T_1, \cdots, T_N を次の漸化式に従って計算します。

 \displaystyle
\begin{align}
T_0(x) &= 1 \\
T_1(x) &= x \\
T_{k+1}(x) &= 2xT_k(x) - T_{k-1}(x)
\end{align}

最後、  \displaystyle  f(t) は次の式で計算できます。

 \displaystyle
\begin{align}
f(t) &\approx \sum_{k=0}^{N} c_k T_k \\
\end{align}

サンプルコード

以下は係数  \displaystyle  c_i (cs) から  \displaystyle  f(t) を計算するScalaでのサンプルコードです。xは正規化済みとします。高速化のためScalaらしくないコードです。

  def calcChebyshev(cs: IndexedSeq[Double], x: Double): Double = {
    // x は -1.0 <= x <= +1.0
    val n = cs.size;
    if (n == 1) {
      cs(0);
    } else {
      val x2 = 2.0 * x;
      var result: Double = cs(0) + cs(1) * x;
      var i: Int = 2;
      var t0: Double = 1.0;
      var t1: Double = x;
      while (i < n) {
        val t2 = x2 * t1 - t0;
        result += cs(i) * t2;
        t0 = t1;
        t1 = t2;
        i = i + 1;
      }
      result;
    }
  }

JPLのデータを使うときに必要な計算式です。

JPL Planetary and Lunar Ephemerides

一時クレデンシャルでServerless Frameworkを動かす

AWSのEC2インスタンスにアタッチされたIAM Roleから別のIAM Roleにassumeできるようにしてあって、assume先の権限でServerless Frameworkを動かす必要がある場合、 ~/.aws/config にRoleを書いてもできませんでした。→前回の記事

手動でassumeして、一時クレデンシャル(temporary credentials)を取得して、そのアクセスキーを環境変数に設定すれば、 ~/.aws/config 関係なく、Serverless Frameworkを実行できましたので、メモしておきます。

準備

IAM Roleを2つ準備して、EC2インスタンスも準備して、Roleをアタッチするところまでは前回の記事と同じです。

  • IAM Role sample-ec2-role: なにも権限のないIAM Role。EC2にアタッチ
  • IAM Role sample-admin: S3などServerless Framework実行に必要な権限。sample-ec2-roleからassumeできるようにTrust Relationshipを設定

一時クレデンシャル取得

assume先のIAM Role(以下例ではsample-adminという名前のIAM Role)をARNで指定して、assume-roleコマンドを実行します。

$ aws sts assume-role --role-arn arn:aws:iam::123456789012:role/sample-admin --role-session-name sample-session

すると、以下のJSONが返ってきます。

{
    "Credentials": {
        "AccessKeyId": "ASIAXXXXXXXXXXXXXXXX",
        "SecretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "SessionToken": "xxxxxxxxxxxxxxxx....",
        "Expiration": "2021-04-30T06:14:32Z"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAXXXXXXXXXXXXXXXXX:sample-session",
        "Arn": "arn:aws:sts::123456789012:assumed-role/sample-admin/sample-session"
    }
}

Serverless Framework実行

取得したJSONにあるアクセスキーなどをServerless Frameworkから参照させるために、環境変数を設定します。

$ export AWS_ACCESS_KEY_ID=ASIAXXXXXXXXXXXXXXXX
$ export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$ export AWS_SESSION_TOKEN="xxxxxxxxxxxxxxxx...."

実行。

$ serverless deploy

無事できました。

credential_sourceを使ったAWSのprofileではServerless Frameworkは動かせないのか

EC2インスタンスにアタッチされたIAM Roleから別のIAM Roleにassumeできるようにしてあって ~/.aws/config に以下のように書いてある場合、

[profile admin]
role_arn = arn:aws:iam::123456789012:role/sample-admin
credential_source = Ec2InstanceMetadata

awscliでは aws --profile admin ... とすれば sample-admin というIAM Roleでawscliを実行できますが、 serverless deploy --aws-profile admin で動かす方法がわかりませんでした。。。という話。

やったこと

IAM Role 2つ準備

sample-ec2-role

sample-ec2-role という名前の、なにも権限のないIAM Role。EC2インスタンスにアタッチしておきます。

sample-admin

sample-admin という名前のIAM Role。AdministratorAccess のPolicyを付与。

Trust Relationshipには以下のように sample-ec2-role からassumeできるようにします。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/sample-ec2-role"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

~/.aws/config 作成

EC2インスタンス~/.aws/config に以下の内容を記述します。 ~/.aws/credentials は作成しません。

[profile admin]
role_arn = arn:aws:iam::123456789012:role/sample-admin
credential_source = Ec2InstanceMetadata

awscliで確認

profileになにも指定しないと、EC2インスタンスにアタッチされているなにも権限のないIAM Roleになるのが確認できます。

$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************RLXK         iam-role
secret_key     ****************g+L7         iam-role
    region                <not set>             None    None

$ aws s3 ls

An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

--profile admin を付ければ、強い権限のIAM Roleになります。

$ aws --profile admin configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                    admin           manual    --profile
access_key     ****************HZ7K      assume-role
secret_key     ****************Lr2q      assume-role
    region           ap-northeast-1      config-file    ~/.aws/config

$ aws --profile admin s3 ls
2020-11-03 15:45:51 xxxx
2020-07-23 18:09:47 xxxx
....

serverlessで確認

まずはprofileなしで実行。EC2インスタンスにアタッチされているなにも権限のないIAM Roleになるので、権限が足りないというエラーになります。

$ serverless deploy -v
Serverless: Packaging service...
Serverless: Excluding development dependencies...

 Serverless Error ----------------------------------------

  Could not locate deployment bucket. Error: Access Denied

  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com

  Your Environment Information ---------------------------
     Operating System:          linux
     Node Version:              10.19.0
     Framework Version:         2.38.0
     Plugin Version:            4.5.3
     SDK Version:               4.2.2
     Components Version:        3.9.0

--aws-profile admin を指定してみると、 Could not locate deployment bucket. Error: AWS profile "admin" doesn't seem to be configured というよくわからないエラーになりました。

$ serverless deploy --aws-profile admin -v
Serverless: Packaging service...
Serverless: Excluding development dependencies...

 Serverless Error ----------------------------------------

  Could not locate deployment bucket. Error: AWS profile "admin" doesn't seem to be configured

  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com

  Your Environment Information ---------------------------
     Operating System:          linux
     Node Version:              10.19.0
     Framework Version:         2.38.0
     Plugin Version:            4.5.3
     SDK Version:               4.2.2
     Components Version:        3.9.0

結論

このエラーメッセージから参考記事を探してみると、 ~/.aws/credentials を作ればいいとか、 AWS_SDK_LOAD_CONFIG を指定すればいいとかいう記事も見つかりますけど、いろいろな組み合わせを試しましたが、方法を見つけられませんでした。

最終的に credential_sourceJavaScriptで実装されているServerless Frameworkでは対応していない、というコメントが見つかりました。

Assumed role not found when defined in ~/.aws/config · Issue #5048 · serverless/serverless

残念。

2021/04/30 追記

一時クレデンシャルを取得して環境変数に設定すれば、できました。

suzuki-navi.hatenablog.com

AWS SESでPythonからメール送信してみる

AWSのSES (Amazon Simple Email Service)を初めて使ってみました。

使い方は以下の公式ドキュメントがわかりやすかったです。

AWS SDK for Python (Boto) を使用して E メールを送信する - Amazon Simple Email Service

1. メールアドレス登録

メール送信時の差出人アドレスは事前にSESに登録して、メールアドレスの持ち主であることの確認をします。

SESのマネジメントコンソールから登録できます。

f:id:suzuki-navi:20210424212019p:plain

2. メール送信のPythonコード

import json

import boto3
from botocore.exceptions import ClientError

profile = "default"
region = "ap-northeast-1"

session = boto3.session.Session(profile_name = profile, region_name = region)
client = session.client("ses")

email_from = "Sample <sample@gmail.com>"
email_to = "sample_to@gmail.com"
charset = "UTF-8"

subject = "Amazon SES Test (SDK for Python)"

body_text = "Hello, SES!"

body_html = """<html>
<head></head>
<body>
  <p>Hello, <a href='https://aws.amazon.com/ses/'>Amazon SES</a>!</p>
</body>
</html>
"""

try:
    res = client.send_email(
        Source = email_from,
        Destination = {
            "ToAddresses": [
                email_to,
            ],
        },
        Message = {
            "Body": {
                "Html": {
                    "Charset": charset,
                    "Data": body_html,
                },
                "Text": {
                    "Charset": charset,
                    "Data": body_text,
                },
            },
            "Subject": {
                "Charset": charset,
                "Data": subject,
            },
        },
    )

except ClientError as e:
    print(e.response["Error"]["Message"])
else:
    print(json.dumps(res))

実行結果の例 (JSONをjqコマンドで整形してあります)

{
  "MessageId": "0106017903be5694-75c47c76-09fb-4da8-a430-1178fb4c1c04-000000",
  "ResponseMetadata": {
    "RequestId": "fd839fd9-914b-46bd-bb72-403c3da12446",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "date": "Sat, 24 Apr 2021 11:57:56 GMT",
      "content-type": "text/xml",
      "content-length": "326",
      "connection": "keep-alive",
      "x-amzn-requestid": "fd839fd9-914b-46bd-bb72-403c3da12446"
    },
    "RetryAttempts": 0
  }
}

3. 添付ファイル付きメール送信のPythonコード

ファイル添付も以下の公式ドキュメントがわかりやすかったです。

Amazon SES API を使用して raw E メールを送信する - Amazon Simple Email Service

Pythonemail.mime というパッケージを使うのですが、このパッケージに関する公式ドキュメントはこちら。

email.mime: Creating email and MIME objects from scratch — Python 3.8.9 documentation

boto3の send_email の代わりに send_raw_email というメソッドを使います。 email.mimeエンコードしたメール本文を send_raw_email でメール送信します。

import json

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

import boto3
from botocore.exceptions import ClientError

profile = "default"
region = "ap-northeast-1"

session = boto3.session.Session(profile_name = profile, region_name = region)
client = session.client("ses")

email_from = "Sample <sample@gmail.com>"
email_to = "sample_to@gmail.com"
charset = "UTF-8"

subject = "Amazon SES Test (SDK for Python)"

body_text = "Hello, SES!"

body_html = """<html>
<head></head>
<body>
  <p>Hello, <a href='https://aws.amazon.com/ses/'>Amazon SES</a>!</p>
</body>
</html>
"""

msg = MIMEMultipart('mixed')
msg['Subject'] = subject
msg['From'] = email_from
msg['To'] = email_to

msg_body = MIMEMultipart('alternative')
msg_body.attach(MIMEText(body_text.encode(charset), 'plain', charset))
msg_body.attach(MIMEText(body_html.encode(charset), 'html', charset))
msg.attach(msg_body)

# ローカルにある sample.txt をファイル添付
attachment = MIMEApplication(open("sample.txt", 'rb').read())
attachment.add_header('Content-Disposition', 'attachment', filename = "sample.txt")
msg.attach(attachment)

print(msg.as_string())

try:
    res = client.send_raw_email(
        Source = email_from,
        Destinations = [email_to],
        RawMessage = {
            'Data': msg.as_string(),
        },
    )

except ClientError as e:
    print(e.response["Error"]["Message"])
else:
    print(json.dumps(res))

以下は、 email.mimeエンコードした結果( msg.as_string() の部分の出力)の例です。

Content-Type: multipart/mixed; boundary="===============1385664818049781378=="
MIME-Version: 1.0
Subject: Amazon SES Test (SDK for Python)
From: Sample <sample@gmail.com>
To: sample_to@gmail.com

--===============1385664818049781378==
Content-Type: multipart/alternative; boundary="===============0301123813487367193=="
MIME-Version: 1.0

--===============0301123813487367193==
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: base64

SGVsbG8sIFNFUyE=

--===============0301123813487367193==
MIME-Version: 1.0
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64

PGh0bWw+CjxoZWFkPjwvaGVhZD4KPGJvZHk+CiAgPHA+SGVsbG8sIDxhIGhyZWY9J2h0dHBzOi8v
YXdzLmFtYXpvbi5jb20vc2VzLyc+QW1hem9uIFNFUzwvYT4hPC9wPgo8L2JvZHk+CjwvaHRtbD4K

--===============0301123813487367193==--

--===============1385664818049781378==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="sample.txt"

SGVsbG8K

--===============1385664818049781378==--

AWSのECR(Container Registry)にHello, WorldなDockerイメージを置いてみる

Amazon Elastic Container Registry(ECR)を初めて触ってみます。

(前回Google Container Registryを触ってました。)

Dockerコンテナイメージ作成

以下のような適当なDockerfileを作成します。

FROM ubuntu:20.04
RUN echo "Hello, world!" > /hello.txt

コンテナイメージをビルドします。Dockerfile のあるディレクトリで以下のコマンドでビルドできます。

$ docker build -t hello-world .

作成したコンテナイメージは、docker imagesコマンドで見えるようになります。

$ docker images
REPOSITORY                                                      TAG                  IMAGE ID            CREATED             SIZE
hello-world                                                     latest               a94c81bcd9f5        10 minutes ago      72.9MB

試しにコンテナイメージを実行してみます。

$ docker run --rm hello-world cat /hello.txt
Hello, world!

AWSのECR(Container Registry)にpush

まずはcreate-repositoryします。

$ aws ecr create-repository --repository-name hello-world

aws ecr describe-repositoriesコマンドでリポジトリに作成できているのが確認できます。

$ aws ecr describe-repositories --repository-names hello-world
{
    "repositories": [
        {
            "repositoryArn": "arn:aws:ecr:ap-northeast-1:xxxxxxxxxxxx:repository/hello-world",
            "registryId": "xxxxxxxxxxxx",
            "repositoryName": "hello-world",
            "repositoryUri": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world",
            "createdAt": 1618978639.0,
            "imageTagMutability": "MUTABLE",
            "imageScanningConfiguration": {
                "scanOnPush": false
            },
            "encryptionConfiguration": {
                "encryptionType": "AES256"
            }
        }
    ]
}

aws ecr list-imagesコマンドで見ると、まだイメージがないのがわかります。

$ aws ecr list-images --repository-name hello-world
{
    "imageIds": []
}

pushするために、タグを付けます。新しいタグの名前は ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/NAME というフォーマットです。ACCOUNT_IDは12桁の数字が入ります。

$ docker tag hello-world xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world

docker imagesコマンドでタグ付けした結果を確認できます。

$ docker images
REPOSITORY                                                      TAG                  IMAGE ID            CREATED             SIZE
xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world   latest               a94c81bcd9f5        37 minutes ago      72.9MB
hello-world                                                     latest               a94c81bcd9f5        37 minutes ago      72.9MB

このコンテナイメージをECRにアップロードします。

$ $(aws ecr get-login --no-include-email)
$ docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world

aws ecr get-login --no-include-emailコマンドは docker login -u AWS -p ........ という出力をするので、それをそのまま実行すればdocker loginできるようです。

pushできたことは、aws ecr list-imagesコマンドで確認できます。

$ aws ecr list-images --repository-name hello-world
{
    "imageIds": [
        {
            "imageDigest": "sha256:7ad2413ef0a6c81e07ab1fc21e1fa2253f78c3ecae3126f539e96cfab2d624f6",
            "imageTag": "latest"
        }
    ]
}

AWSのECR(Container Registry)からpull

別の環境からpullにするには改めてdocker loginが必要です。

$ $(aws ecr get-login --no-include-email)

ログインできていれば、以下のコマンドで、pullして実行を確認できます。

$ docker run --rm xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world cat /hello.txt
Hello, world!

バージョン情報

$ docker --version
Docker version 19.03.8, build afacb8b7f0
$ aws --version
aws-cli/1.18.209 Python/3.8.5 Linux/5.4.0-1036-gcp botocore/1.19.49

aws-cliのバージョンが2ではdocker loginのコマンドが違うようです。