ECS(Fargate)で動かすコンテナにSSMからクレデンシャル情報を渡す
発表時の資料
本記事に関する発表資料はこちらです。
※ プラットフォームバージョン1.3以上の場合
この記事はプラットフォーム1.3より前を使用する前提で記載しています。 1.3以上をお使いになる場合は、こちらの記事をご覧ください。
https://devblog.thebase.in/entry/2019/01/16/110000
目次
- クレデンシャル情報の扱い方
- 概要構成
- 具体的な手順
- 参考資料
クレデンシャル情報の扱い方
クレデンシャル情報の扱い方を考えるに当たり、Beyond the Twelve-Factor Appというクラウドネイティブアプリケーションの設計パターンについて説明した資料がよく参照されています。
上記の資料内の「05. CONFIGURATION, CREDENTIALS, AND CODE」という章で次のように記載されています。
「今すぐにでもコードをオープンソース化できるかどうか」というのがわかりやすい指標ですね。こちらで推奨されている環境変数への格納を今回進めていきます。
寄り道:環境変数への格納について検討
環境変数への格納について、「セキュリティ観点」・「管理観点」の両者で検討してみます。
セキュリティ観点
セキュリティ観点的には、必要以上に参照可能な状態を避ける必要があります。例えば、「/etc/environmentにクレデンシャル情報を書いてそれを使う」といったことをした場合は、アプリケーションの動作に必要なプロセス以外からもクレデンシャル情報を読むことができます。
今回のケースでは、コンテナプロセス上でアプリケーションを動かすケースのため、コンテナプロセスに対して環境変数を注入することになります。それであれば、必要なプロセス以外からのクレデンシャル情報へのアクセスはある程度避けることができそうです。
管理観点
YAMLやTOMLファイルで設定する際、次のように要素に応じて階層的に管理したいというニーズがあると思います。
[database1] user = "sample_user1" password = "sample_password1" host = "database1" port = 3306 name = "sample1" [database2] user = "sample_user2" password = "sample_password2" host = "database2" port = 3306 name = "sample2"
環境変数での注入の場合、単に環境変数に直接書くのであれば階層をもたせることは少し難しそうですが、AWSであればAWS Systems Manager パラメータストアを用いることで階層管理を実現できそうです。
AWS Systems Manager パラメータストア は、設定データ管理と機密管理のための安全な階層型ストレージを提供します。パスワード、データベース文字列、ライセンスコードなどのデータをパラメータ値として保存することができます。
概要構成
今回の概要構成は下図のものです。
大まかな構成要素は以下になります。
略語 | 正式名称 | 概要 |
---|---|---|
ECS | Amazon Elastic Container Service | Docker コンテナをサポートする拡張性とパフォーマンスに優れたコンテナオーケストレーションサービス |
ECR | Amazon Elastic Container Registry | 完全マネージド型の Docker コンテナレジストリ |
Fargate | AWS Fargate | ECS/EKS内のテクノロジー、サーバーやクラスターを管理することなくコンテナを実行できるようになる |
IAM | AWS Identity and Access Management | AWS リソースへのアクセスを安全に制御するためのウェブサービス |
Parameter Store | AWS Systems Manager Paramter Store | 設定データ管理と機密管理のための安全な階層型ストレージ |
KMS | AWS Key Management Service | データの暗号化に使用するキーの容易な作成および管理 |
ALB | Application Load Balancer | L4/L7で機能するロードバランサー |
具体的な手順
今回のサンプルは下記のgithubレポジトリに公開しています。
※ 今回のサンプルをそのまま試しに実行する場合は、なにかしらのMySQLサーバを準備する必要があります。サンプルを実行したい場合は、RDSの無料枠などを利用して接続可能なデータベースを用意してください。
手順一覧
- KMSで暗号化キーの作成
- Parameterの登録
- IAMロールの作成
- IAMポリシーの作成
- IAMロールにIAMポリシーをアタッチ
- Containerイメージの作成・プッシュ
- ECSのタスク定義
- タスク実行
KMSで暗号化キーの作成
まずは、クレデンシャル情報を暗号化するためのキーをKMSで作成します。作成に当たり、AWS CLIのcreate-keyコマンドを実行します。
$ aws kms create-key --description go-ecs-sample --region ap-northeast-1 KEYMETADATA < AWSAccountId > arn:aws:kms:ap-northeast-1:< AWSAccountId >:key/< KeyId > go-ecs-sample True < KeyId > CUSTOMER Enabled ENCRYPT_DECRYPT AWS_KMS
上記コマンドを実行すると、CMK(customer master key)というデータの暗号化に用いるキーが作られます。後続で実行する作業にて、< AWSAccountId >
・< KeyId >
を使っていきます。
Parameterの登録
次に、Parameter Storeにクレデンシャル情報を登録していきます。今回は、RDSへの接続情報を登録していきます。
$ aws ssm put-parameter --name /goecssample/database/sample/master/user --type "String" --value "user" --description "データベースのmasterユーザー名" --region ap-northeast-1 $ aws ssm put-parameter --name /goecssample/database/sample/master/password --type "SecureString" --value "password" --key-id "< KeyId >" --description "データベースのmasterユーザーパスワード" --region ap-northeast-1 $ aws ssm put-parameter --name /goecssample/database/sample/master/host --type "String" --value "host" --description "データベースのmasterホスト名" --region ap-northeast-1 $ aws ssm put-parameter --name /goecssample/database/sample/master/name --type "String" --value "sample" --description "データベースのmasterデータベース名" --region ap-northeast-1 $ aws ssm put-parameter --name /goecssample/database/sample/master/port --type "String" --value "3306" --description "データベースの接続ポート" --region ap-northeast-1
上記のコマンドを実行すると次のようなレスポンスが返ってきます。
1 1 1 1 1
このレスポンス結果は、各パラメータのバージョンを表しています。これは--overwrite
オプションを付けて上書きしたりすると、2・3と増えていきます。
また、環境変数を扱う上での階層管理ですが、Parameter Storeを使う場合は、/stage1/stage2/stage3
という形で階層を区切ることが出来るので、今回活用しています。
AWS System Manager: パラメータを階層に編成
IAMロールの作成
次に、ECSタスクのタスクロールとなるIAMロールを作成します。タスク用のIAMロールを作成し、タスク定義に当該IAMロールを定義することによって、IAMロールの認証情報にアクセスすることができるようになります。今回、Parameter Store・KMSにアクセス可能なIAMロールを作成して、ECSのタスク定義に定義することを目指します。
まず、IAMロールの定義をjsonで作成します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
作成したファイルをもとにIAMロールを作成します。
$ aws iam create-role --role-name go-ecs-sample --assume-role-policy-document file://ecs-tasks-trust-policy.json ROLE arn:aws:iam::< AWSAccountId >:role/go-ecs-sample 2018-08-24T02:10:14Z / HFJKHSYGHOSUHG... go-ecs-sample
IAMポリシーの作成
次に、IAMポリシーを作成します。まずは次のようなjsonファイルを作成します。ここでは、「Parameter Storeのgoecssample/
以下の階層のパラメータを取得する」・「今回作成したKMSキーで暗号化したものを複合する」ことを許可しています。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:DescribeParameters" ], "Resource": "*" }, { "Sid": "Stmt1482841904000", "Effect": "Allow", "Action": [ "ssm:GetParameters" ], "Resource": [ "arn:aws:ssm:ap-northeast-1:< AWSAccountId >:parameter/goecssample/*" ] }, { "Sid": "Stmt1482841948000", "Effect": "Allow", "Action": [ "kms:Decrypt" ], "Resource": [ "arn:aws:kms:ap-northeast-1:< AWSAccountId >:key/< KeyId >" ] } ] }
作成したファイルをもとにIAMロールを作成します。
$ aws iam create-policy --policy-name go-ecs-sample --policy-document file://go-ecs-secret-access.json POLICY arn:aws:iam::< AWSAccountId >:policy/go-ecs-sample 0 2018-08-24T02:13:46Z v1 True / 0 HFJKHSYGHOSUHG... go-ecs-sample 2018-08-24T02:13:46Z
IAMロールにIAMポリシーをアタッチ
作成したIAMロールにIAMポリシーをアタッチします。
$ aws iam attach-role-policy --role-name go-ecs-sample --policy-arn "arn:aws:iam::< AWSAccountId >policy/go-ecs-sample"
Dockerイメージの作成・プッシュ
ここまででParameter Storeから必要な情報を取る準備はできたので、次にECSタスクで実行するためのDockerイメージを作成していきます。今回のサンプルは、MySQLサーバに接続可能であればサーバが立ち上がるAPIです。
https://github.com/Khigashiguchi/go-ecs-example/blob/master/main.go
func main() { var err error // Get configuration conf, err := config.NewConfig() if err != nil { fmt.Fprintf(os.Stderr, "failed to get configuration: %s", err) panic(err.Error()) } // Get database Handle db, err := NewDB(conf.DB) if err != nil { fmt.Fprintf(os.Stderr, "failed to get connection with database: %s", err) panic(err.Error()) } // Router r := mux.NewRouter() h := Handler{DB: db} r.Methods("GET").Path("/posts").HandlerFunc(h.GetPostsHandler) r.Methods("GET").Path("/.healthcheck").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) // Serve HTTP service fmt.Fprint(os.Stdout, ">> Start to listen http server post :80\n") if err = http.ListenAndServe(":80", r); err != nil { fmt.Fprintf(os.Stderr, "failed to start http server: %s", err) panic(err.Error()) } }
上記の簡単なAPIを動かすために次のようなDockerfileを書きます。ENTRYPOINTからaws-cliを叩くためにawscliをインストールしています。
https://github.com/Khigashiguchi/go-ecs-example/blob/master/Dockerfile
FROM golang:1.10-alpine3.7 WORKDIR /go/src/github.com/Khigashiguchi/go-ecs-example/ COPY . /go/src/github.com/Khigashiguchi/go-ecs-example/ RUN apk add --no-cache ca-certificates \ dpkg \ gcc \ git \ musl-dev \ openssh \ bash \ curl \ python # Install the AWS CLI # https://aws.amazon.com/jp/blogs/news/managing-secrets-for-amazon-ecs-applications-using-parameter-store-and-iam-roles-for-tasks/ RUN curl -O https://bootstrap.pypa.io/get-pip.py RUN python get-pip.py RUN pip install awscli RUN go get -u github.com/golang/dep/cmd/dep RUN dep ensure RUN go build -v -o server EXPOSE 80 ENTRYPOINT ["./docker-entrypoint.sh"] CMD ["./server"]
ENTRYPOINTで指定しているdocker-entrypoint.sh
は次のようなものです。ローカル環境では叩きに行かずAWS ECS上でのデプロイ時のみParameter Storeにアクセスしたいため、ECS上でのデプロイ時のみ事前付与する環境変数、PARAMETER_STORE_PREFIX
を用意しています。
#!/usr/bin/env bash set -e PARAMETER_STORE_PREFIX=${PARAMETER_STORE_PREFIX:-} if [ -n "$PARAMETER_STORE_PREFIX" ]; then export DB_USER=$(aws ssm get-parameters --name /${PARAMETER_STORE_PREFIX}/database/sample/master/user --query "Parameters[0].Value" --region ap-northeast-1 --output text) export DB_PASSWORD=$(aws ssm get-parameters --name /${PARAMETER_STORE_PREFIX}/database/sample/master/password --with-decryption --query "Parameters[0].Value" --region ap-northeast-1 --output text) export DB_HOST=$(aws ssm get-parameters --name /${PARAMETER_STORE_PREFIX}/database/sample/master/host --query "Parameters[0].Value" --region ap-northeast-1 --output text) export DB_NAME=$(aws ssm get-parameters --name /${PARAMETER_STORE_PREFIX}/database/sample/master/name --query "Parameters[0].Value" --region ap-northeast-1 --output text) export DB_PORT=$(aws ssm get-parameters --name /${PARAMETER_STORE_PREFIX}/database/sample/master/port --query "Parameters[0].Value" --region ap-northeast-1 --output text) fi exec "$@"
これを、ECRのレポジトリにイメージプッシュします。レポジトリの作成手順は下記をご参照ください。
$ $(aws ecr get-login --no-include-email --region ap-northeast-1) $ docker build -t go-ecs-sample . $ docker tag go-ecs-sample:latest < AWSAccountId >.dkr.ecr.ap-northeast-1.amazonaws.com/go-ecs-sample:latest $ docker push < AWSAccountId >.dkr.ecr.ap-northeast-1.amazonaws.com/go-ecs-sample:latest
ECSのタスク定義
ECSのタスク定義を作成します。タスク定義の詳細の説明については、下記の公式ドキュメントをご参照ください。
要点としては、下記2点です。
- タスクロールに今回作成したIAMロール
`go-ecs-example
を指定してください。
- タスクロールに今回作成したIAMロール
なお、タスク実行ロールはデフォルトのecsTaskExecutionRole
でOKです。
そして、コンテナ定義で設定する環境変数に次の内容をセットしてください。
環境変数 | 値 |
---|---|
PARAMETER_STORE_PREFIX | goecssample |
タスク実行
タスクを登録するとタスクが実行されます。
前回のステータス
がRunning
になっていれば、OKです。