クラウドインテグレーション2部技術3課の山下です。
今回は、Amazon Elastic Container Service(以下、ECS)の タスクロールとタスク実行ロールの違いについて、 簡単なサンプルアプリを用意して検証してみたいと思います。
- (背景) タスクロールとタスク実行ロールの違いがピンと来なかった
- (結論) アプリのコードでAWSリソースへアクセスする場合、タスクロールに権限をつける
- 検証してみる
- パターン1:コンテナ上のアプリケーションからSecrets Managerにアクセスする
- パターン2:コンテナ定義の環境変数にSecrets Managerのシークレット情報を格納する
- おわりに
(背景) タスクロールとタスク実行ロールの違いがピンと来なかった
初めて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用のサンプルアプリは以下です。今回は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 | 失敗 |
成功・失敗時それぞれのログは下図です。
パターン2:コンテナ定義の環境変数にSecrets Managerのシークレット情報を格納する
コンテナ定義の環境変数にSecrets Managerのシークレット情報を格納する方法です。 この場合、タスク実行ロールにSecrets Managerにアクセスするポリシーが必要です。
コンテナ定義
パターン2の場合、ECSのコンテナ定義で環境変数を設定し、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の場合、失敗時にはログに何も出力されませんでした。タスクの詳細画面を見ると、タスクが自動で停止され、停止理由にSecrets Managerにアクセスできなかった旨が記載されていました。
タスク実行前に、コンテナエージェントによるAWSリソースへのアクセスが失敗したため、ログに何も出力されなかったようです。
おわりに
タスクロールとタスク実行ロール、それぞれの権限を使ってSecrets Managerの情報を取得してみました。 実際にサンプルアプリを用意して、同じリソースへアクセスしてみることで、両者の違いがイメージしやすくなりました。
同じようなお悩みを持った方に、この記事が少しでも参考になれば幸いです。
山下 祐樹(執筆記事の一覧)
2021年11月中途入社。前職では情シスとして社内ネットワークの更改や運用に携わっていました。 2023 Japan AWS All Certifications Engineers。