AWS Copilot のタスク実行時に AWS リソースから情報を取得する

[AWS Containers Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-containers) の11日目の記事です。

唐突ですが、先月に GA した [AWS Copilot](https://aws.amazon.com/jp/blogs/containers/aws-copilot-is-now-generally-available/) を使ってみようかなと考えている方もいるのではないでしょうか。

Web サービスなどを構築・運用している場合、機能追加やバグ修正のためにデータベーススキームを変更するなどの低頻度で実行するタスクのために、Copilot task run が活用できるかと思います。ただし、セキュリティの観点からクレデンシャルなどの情報をコンテナ内に格納したり環境変数としてコンテナに引き渡すなどは避けたい場面もあるでしょう。
この記事では、 Copilot の run task 時にタスクロールを付与し、実行コンテナ内で処理するために必要な情報を AWS Systems Manager Parameter Store や AWS Secrets Manager から取得するやつをやってみます。

かなり手を抜いて、AWS CLI をコンテナ内で実行し、 AWS Systems Manager Parameter Store から事前登録している情報を取得するまでをやってみます。

構成
---
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939177/69a9c5bb-bc56-d6ce-4840-4726444257a2.png">

- VPC 周り及び作業環境の EC2 は事前に作成済みとします
- EC2 には事前に AdministratorAccess ポリシーがアタッチされている ロールを付与します

準備
---
作業端末から Copilot 実行環境である EC2 へ ssh 接続し、 [Copilot のインストール](https://aws.github.io/copilot-cli/docs/getting-started/install/) を行います。

デフォルト・リージョンを指定するために、なんらかの方法 (`vi` や `aws configure`) で以下ファイルを作成します。
```shell_session
$ cat .aws/config
[default]
region = us-east-1
```

次に `copilot app init` を実行し、 ECS クラスターを構築します。 ECS クラスターの構築と同時に、イメージとして nginx を指定した sample-front サービスも構築します。

``` shell-session
$ copilot app init
Application name: sample
✔ Created the infrastructure to manage services and jobs under application sample.

✔ The directory copilot will hold service manifests for application sample.

Recommended follow-up actions:
- Run `copilot init` to add a new service or job to your application.
[ec2-user@ip-10-0-0-137 ~]$ copilot init
Note: It's best to run this command in the root of your Git repository.
Welcome to the Copilot CLI! We're going to walk you through some questions
to help you get set up with an application on ECS. An application is a collection of
containerized services that operate together.

Workload type: Load Balanced Web Service
Service name: sample-front
Dockerfile: Use an existing image instead
Image: nginx
Port:
Ok great, we'll set up a Load Balanced Web Service named sample-front in application sample listening on port 80.

✔ Created the infrastructure to manage services and jobs under application sample.

✔ Wrote the manifest for service sample-front at copilot/sample-front/manifest.yml
Your manifest contains configurations like your container size and port (:80).

✔ Created ECR repositories for service sample-front.

All right, you're all set for local development.
Deploy: Yes

✔ Created the infrastructure for the test environment.
- Virtual private cloud on 2 availability zones to hold your services and jobs     [Complete]
  - Internet gateway to connect the network to the internet                        [Complete]
  - Public subnets for internet facing services and jobs                           [Complete]
  - Private subnets for services and jobs that can't be reached from the internet  [Complete]
  - Routing tables for services and jobs to talk with each other                   [Complete]
- ECS Cluster to hold your services and jobs                                       [Complete]
✔ Linked account 123456789012 and region us-east-1 to application sample.

✔ Created environment test in region us-east-1 under application sample.
Note: Couldn't find a git commit sha to use as an image tag. Are you in a git repository?
Input an image tag value: nginx
Environment test is already on the latest version v1.1.0, skip upgrade.


✔ Deployed sample-front, you can access it at http://sampl-Publi-FTCDQO5GGNRW-115515095.us-east-1.elb.amazonaws.com.
```

次は、ECS タスク実行時に指定するための IAM ロールを作成します。 IAM ロール作成のために Trust Policy が記述されている JSON ファイルを CLI 実行前に作成しておきます。

```
$ cat copilot-sample-task-role-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
$ aws iam --region us-east-1 create-role --role-name copilot-sample-task-role --assume-role-policy-document file://copilot-sample-task-role-trust-policy.json
{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Principal": {
                        "Service": "ecs-tasks.amazonaws.com"
                    },
                    "Effect": "Allow",
                    "Sid": ""
                }
            ]
        },
        "RoleId": "AROAUJDL34IZZ6GBAD7JY",
        "CreateDate": "2020-12-11T13:03:33Z",
        "RoleName": "copilot-sample-task-role",
        "Path": "/",
        "Arn": "arn:aws:iam::123456789012:role/copilot-sample-task-role"
    }
}
$ cat copilot-sample-task-role-inline-policyy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ssm:GetParameter"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
$ aws iam --region us-east-1 put-role-policy --role-name copilot-sample-task-role --policy-name copilot-sample-task-role-policy --policy-document file://copilot-sample-task-role-inline-policyy.json
```

最後に、AWS Systems Manager Parameter Store へコンテナから取得するパラメータを設定します。

``` shell-sesion
$ aws ssm --region us-east-1 put-parameter --name test-name --value test-value --type String
{
    "Tier": "Standard",
    "Version": 1
}
```


実行
---

準備できたと思うので、 `copilot task run` コマンドを実行します。コンテナ・イメージは、 AWS が提供している amazon/aws-cli イメージを利用します。

```
$ copilot task run --app sample --command 'ssm --region us-east-1 get-parameter --name test-name' --image 'amazon/aws-cli' --task-role arn:aws:iam::123456789012:role/copilot-sample-task-role
In which environment would you like to run this task? test
✔ Successfully provisioned task resources.

✘ Failed to run ec2-user.

✘ run task ec2-user: run task ec2-user: run task(s) copilot-ec2-user: AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/sample-test-EnvManagerRole/1607694710020885474 is not authorized to perform: iam:PassRole on resource: arn:aws:iam::123456789012:role/copilot-sample-task-role
```

失敗です。コマンド実行しタスクを起動すると、 sample-test-EnvManagerRole という IAM ロールから copilot コマンドの引数に指定した `--task-role` へロールが切り替わるところで失敗しているようです。
エラー回避のために、 Copilot コマンドで作成された sample-test-EnvManagerRole ロールに iam:PassRole 権限を付与します。

```shell-session
$ cat copilot-EnvManagerRole-inline-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "iam:PassRole"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
$ aws iam --region us-east-1 put-role-policy --role-name sample-test-EnvManagerRole --policy-name copilot-task-run --policy-document file://copilot-EnvManagerRole-inline-policy.json
```

再度、`copilot task run` コマンドを実行します。

```shell-session
$ copilot task run --app sample --command 'ssm --region us-east-1 get-parameter --name test-name' --image 'amazon/aws-cli' --task-role arn:aws:iam::123456789012:role/copilot-sample-task-role --follow
In which environment would you like to run this task? test
✔ Successfully provisioned task resources.

✔ Task ec2-user is running.

copilot-task/ec2-user/976 {
copilot-task/ec2-user/976     "Parameter": {
copilot-task/ec2-user/976         "Name": "test-name",
copilot-task/ec2-user/976         "Type": "String",
copilot-task/ec2-user/976         "Value": "test-value",
copilot-task/ec2-user/976         "Version": 1,
copilot-task/ec2-user/976         "LastModifiedDate": "2020-12-11T12:55:49.656000+00:00",
copilot-task/ec2-user/976         "ARN": "arn:aws:ssm:us-east-1:123456789012:parameter/test-name",
copilot-task/ec2-user/976         "DataType": "text"
copilot-task/ec2-user/976     }
copilot-task/ec2-user/976 }
Task has stopped.
```

おわりに
---
無事、タスクを起動し AWS Systems Manager Parameter Stoer から事前登録済みの値を取得することができました。
もっとスマートな方法がありそうなので、またの機会に IAM ロール設定まわりを CLI ではなく Copilot コマンドで設定する方法を調べてみたいと思います。