【ECS】タスクロールとタスク実行ロールの違いをサンプルアプリで検証してみた

記事タイトルとURLをコピーする

クラウドインテグレーション2部技術3課の山下です。

今回は、Amazon Elastic Container Service(以下、ECS)の タスクロールとタスク実行ロールの違いについて、 簡単なサンプルアプリを用意して検証してみたいと思います。

(背景) タスクロールとタスク実行ロールの違いがピンと来なかった

初めてECSを触った際、タスクロールとタスク実行ロールの違いが、個人的にいまいちピンと来ませんでした。 公式ドキュメントの説明としては以下のようになります。

タスクロール

Amazon ECS タスクには IAM ロールを関連付けることができます。IAM ロールで付与される許可は、タスクで実行されているコンテナによって引き受けられます。

コンテナ化したアプリケーションは AWS API を呼び出す必要がある場合、AWS 認証情報でそれらの AWS API リクエストに署名する必要があります。なお、タスクの IAM ロールは、アプリケーションを使用するための認証情報を管理する戦略を利用できます。

タスク用の IAM ロール - Amazon Elastic Container Service


タスク実行ロール

タスク実行ロールは、ユーザーに代わって AWS API コールを実行するためのアクセス許可を Amazon ECS コンテナと Fargate エージェントに付与します。

Amazon ECS タスク実行IAM ロール - Amazon Elastic Container Service

(結論) アプリのコードでAWSリソースへアクセスする場合、タスクロールに権限をつける

ザックリ言うと、コンテナ内のアプリに、AWSリソースへアクセスするためのコードが直接書かれている場合、タスクロールに権限を付ける必要があります。

なお、タスク実行ロールに権限をつけるケースについてもザックリ言うと、ECS・Fargate上の設定でAWSリソースにアクセスする場合です。 例として、コンテナ定義の設定で以下を設定した場合等が該当します。

・CloudWatchへのログ保存を設定
・コンテナイメージにECRのリポジトリを指定
・Secrets Managerのシークレット情報を環境変数に設定

コンテナ定義サンプル

検証してみる

よりイメージをしやすくするために、2つのサンプルアプリを用意し、同じAWSリソースへアクセスしてみます。1つ目はタスクロールの権限を使用し、もう1つはタスク実行ロールの権限を使用します。それぞれ、サンプルアプリの内容がどのように変わるか確認します。

構成

今回は、下図の構成で検証を行います。


AWS Secrets Manager(以下、Secrets Manager)からログイン情報を取得し、その情報を使ってRDSに接続してみます。

データベース

データベースは以下の情報が載っているだけの簡便なものを用意しました。日本の大きい湖トップ5です。(データの内容に深い意味はありません)

データベースの内容(クリックすると展開されます)

MySQL [Lake]> SELECT * FROM Lake;
+----+--------------+-----------------------+--------+
| id | name         | prefectures           | area   |
+----+--------------+-----------------------+--------+
|  1 | 琵琶湖       | 滋賀県                | 669.26 |
|  2 | 霞ヶ浦       | 茨城県                |    220 |
|  3 | サロマ湖     | 北海道                | 151.59 |
|  4 | 猪苗代湖     | 福島県                | 103.24 |
|  5 | 中海         | 島根県・鳥取県        |  85.74 |
+----+--------------+-----------------------+--------+
5 rows in set (0.00 sec)


※データベースに関する補足

データベースが下記ページに記載のエンジンバージョンである場合、IAMデータベース認証が使えます。

MariaDB、MySQL、および PostgreSQL の IAM データベース認証 - Amazon Relational Database Service

IAMデータベース認証を使えばそもそもパスワードを使う必要はないため、上記ページに記載の制限事項に当てはまらなければ、こちらを採用すればよいかと思います。ただ、今回はあくまで検証なので、手元にあったMySQLデータベースでSecrets Managerを利用しています。ご容赦ください。

IAMポリシー

Secrets Managerへの「GetSecretValue」権限を持つIAMポリシーを用意しました。

AccessSecretsManager(クリックすると展開されます)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:*"
            ],
            "Effect": "Allow"
        }
    ]
}


IAMロール

以下の3つのIAMロールを用意しました。

ロール名 ポリシー 概要
ecsTaskRoleGetSecret AccessSecretsManager タスクロール用。Secrets Managerへのアクセス権限あり。
ecsTaskExecutionRole AmazonECSTaskExecutionRolePolicy デフォルトのタスク実行ロール。
Secrets Managerへのアクセス権限なし。
ecsTaskExecutionGetSecret AmazonECSTaskExecutionRolePolicy
AccessSecretsManager
タスク実行ロール用。Secrets Managerへのアクセス権限あり。

パターン1:コンテナ上のアプリケーションからSecrets Managerにアクセスする

コンテナにデプロイされたアプリケーションに、Secrets Managerから情報を取得するコードを記載します。 この場合、タスクロールにSecrets Managerにアクセスするポリシーが必要です。

パターン1のアクセスイメージ


パターン1サンプルアプリ

パターン1用のサンプルアプリは以下です。今回はboto3でSecrets Managerに接続します。 また、DBへのアクセスにはsqlalchemyを使用します。 あくまで検証用のサンプルなので、コードが粗いのはご容赦ください。

パターン1サンプルアプリ(クリックすると展開されます)

# モジュールのインポート
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Float, String, Integer
import boto3
    
# 変数の格納
secret_name = "xxxxxxxxxx" #ここにシークレットの名前を記載
region_name = "ap-northeast-1"
    
# Secrets Managerからシークレット情報を取得
print("Now starting get secret from Secrets Manager...")
session = boto3.session.Session()
client = session.client(service_name='secretsmanager', region_name=region_name)
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
secret_string = get_secret_value_response['SecretString']
secret_dict = eval(secret_string)
    
# Secrets Managerから取得した情報を変数に格納
USER = secret_dict["username"]
PASS = secret_dict["password"]
DBNAME = secret_dict["dbname"]
DBHOST = secret_dict["host"]
    
# DBに接続
print("Now starting access DB...")
engine = create_engine(f'mysql+pymysql://{USER}:{PASS}@{DBHOST}/{DBNAME}')
db_session = scoped_session(sessionmaker(autocommit=False,autoflush=False,bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
    
# モデルの作成
class Lake(Base):
    __tablename__ = 'Lake'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    prefectures = Column(String(64))
    area = Column(Float)
    
    def __init__(self, name=None, prefectures=None, area=None):
        self.name = name
        self.prefectures = prefectures
        self.area = area
    
# importしているモデルに対して、テーブルが存在しなかったら作成する
Base.metadata.create_all(bind=engine)
    
# DBの内容を表示
all_lakes = db_session.query(Lake).all()
for lake in all_lakes:
    print(f'{lake.name} {lake.prefectures} {lake.area}')


検証結果(タスクロールに権限をつけた場合のみ成功)

検証結果としては、タスクロールに「ecsTaskRoleGetSecret」をつけた場合のみ成功、となりました。 タスク実行ロールにつけた権限は関係しませんでした。

検証結果サマリ

検証パターン タスクロール タスク実行ロール 結果
パターン① ecsTaskRoleGetSecret ecsTaskExecutionRole 成功
パターン② ecsTaskRoleGetSecret ecsTaskExecutionGetSecret 成功
パターン③ なし ecsTaskExecutionRole 失敗
パターン④ なし ecsTaskExecutionGetSecret 失敗


成功・失敗時それぞれのログは下図です。

パターン1・成功時のログ


パターン1・失敗時のログ


パターン2:コンテナ定義の環境変数にSecrets Managerのシークレット情報を格納する

コンテナ定義の環境変数にSecrets Managerのシークレット情報を格納する方法です。 この場合、タスク実行ロールにSecrets Managerにアクセスするポリシーが必要です。

パターン2のアクセスイメージ


コンテナ定義

パターン2の場合、ECSのコンテナ定義で環境変数を設定し、Secrets Managerの値を指定します。

コンテナ定義の環境変数にSecrets Managerのシークレット情報を格納


設定の仕方は下記リンクを参照ください。

Secrets Manager を使用した機密データの指定 - Amazon Elastic Container Service

チュートリアル: Secrets Manager のシークレットを使用して機密データを指定する - Amazon Elastic Container Service

サンプルアプリ

パターン2用のサンプルアプリは以下です。 アプリが直接Secrets Managerに接続することはなく、コンテナ定義で設定した環境変数を指定します。そのため、こちらではboto3は使っていません。 あくまで検証用のサンプルなので、コードが粗いのは以下略。

パターン2サンプルアプリ(クリックすると展開されます)

#モジュールのインポート
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Float, String, Integer
import os
    
# 環境変数の取得
USER = os.environ['USER_NAME']
PASS = os.environ['PASSWORD']
DBHOST = os.environ['DB_HOST']
DBNAME = os.environ['DB_NAME']
    
# DBに接続
print("Now starting access DB...")
engine = create_engine(f'mysql+pymysql://{USER}:{PASS}@{DBHOST}/{DBNAME}')
db_session = scoped_session(sessionmaker(autocommit=False,autoflush=False,bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
    
# モデルの作成
class Lake(Base):
    __tablename__ = 'Lake'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), unique=True)
    prefectures = Column(String(64))
    area = Column(Float)
    
    def __init__(self, name=None, prefectures=None, area=None):
        self.name = name
        self.prefectures = prefectures
        self.area = area
    
# importしているモデル全てに対して、テーブルが存在しなかったら作成する
Base.metadata.create_all(bind=engine)
    
# DBの内容を表示
all_lakes = db_session.query(Lake).all()
for lake in all_lakes:
    print(f'{lake.name} {lake.prefectures} {lake.area}')


検証結果(タスク実行ロールに権限をつけた場合のみ成功)

検証結果としては、タスク実行ロールに「ecsTaskExecutionGetSecret」をつけた場合のみ成功、となりました。 タスクロールにつけた権限は関係しませんでした。

検証結果サマリ

検証パターン タスクロール タスク実行ロール 結果
パターン① ecsTaskRoleGetSecret ecsTaskExecutionRole 失敗
パターン② ecsTaskRoleGetSecret ecsTaskExecutionGetSecret 成功
パターン③ なし ecsTaskExecutionRole 失敗
パターン④ なし ecsTaskExecutionGetSecret 成功


成功・失敗時それぞれのログは下図です。

パターン2・成功時のログ


パターン2・失敗時のログ

パターン2の場合、失敗時にはログに何も出力されませんでした。タスクの詳細画面を見ると、タスクが自動で停止され、停止理由にSecrets Managerにアクセスできなかった旨が記載されていました。

パターン2・失敗時のタスク詳細画面

タスク実行前に、コンテナエージェントによるAWSリソースへのアクセスが失敗したため、ログに何も出力されなかったようです。


おわりに

タスクロールとタスク実行ロール、それぞれの権限を使ってSecrets Managerの情報を取得してみました。 実際にサンプルアプリを用意して、同じリソースへアクセスしてみることで、両者の違いがイメージしやすくなりました。

同じようなお悩みを持った方に、この記事が少しでも参考になれば幸いです。