Software engineering from east direction

六本木一丁目で働くソフトウェアエンジニアのブログ

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」という章で次のように記載されています。

f:id:khigashigashi:20180828170659p:plain

「今すぐにでもコードをオープンソース化できるかどうか」というのがわかりやすい指標ですね。こちらで推奨されている環境変数への格納を今回進めていきます。

寄り道:環境変数への格納について検討

環境変数への格納について、「セキュリティ観点」・「管理観点」の両者で検討してみます。

セキュリティ観点

セキュリティ観点的には、必要以上に参照可能な状態を避ける必要があります。例えば、「/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 パラメータストア は、設定データ管理と機密管理のための安全な階層型ストレージを提供します。パスワード、データベース文字列、ライセンスコードなどのデータをパラメータ値として保存することができます。

AWS Systems Manager パラメータストア - AWS Systems Manager

概要構成

今回の概要構成は下図のものです。

f:id:khigashigashi:20180828202554p:plain

大まかな構成要素は以下になります。

略語 正式名称 概要
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レポジトリに公開しています。

github.com

※ 今回のサンプルをそのまま試しに実行する場合は、なにかしらのMySQLサーバを準備する必要があります。サンプルを実行したい場合は、RDSの無料枠などを利用して接続可能なデータベースを用意してください。

手順一覧

  • KMSで暗号化キーの作成
  • Parameterの登録
  • IAMロールの作成
  • IAMポリシーの作成
  • IAMロールにIAMポリシーをアタッチ
  • Containerイメージの作成・プッシュ
  • ECSのタスク定義
  • タスク実行

KMSで暗号化キーの作成

まずは、クレデンシャル情報を暗号化するためのキーをKMSで作成します。作成に当たり、AWS CLIcreate-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のタスク定義に定義することを目指します。

docs.aws.amazon.com

まず、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を用意しています。

https://github.com/Khigashiguchi/go-ecs-example/blob/a2cba61c6c03b6eb22ae449346c51a65d22f5617/docker-entrypoint.sh

#!/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のレポジトリにイメージプッシュします。レポジトリの作成手順は下記をご参照ください。

docs.aws.amazon.com

$ $(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のタスク定義を作成します。タスク定義の詳細の説明については、下記の公式ドキュメントをご参照ください。

docs.aws.amazon.com

要点としては、下記2点です。

    1. タスクロールに今回作成したIAMロール`go-ecs-exampleを指定してください。

f:id:khigashigashi:20180828213458p:plain

なお、タスク実行ロールはデフォルトのecsTaskExecutionRoleでOKです。

f:id:khigashigashi:20180828213439p:plain

そして、コンテナ定義で設定する環境変数に次の内容をセットしてください。

環境変数
PARAMETER_STORE_PREFIX goecssample

タスク実行

タスクを登録するとタスクが実行されます。

f:id:khigashigashi:20180828213958p:plain

前回のステータスRunningになっていれば、OKです。

参考資料

aws.amazon.com

qiita.com

techblog.housmart.co.jp

quipper.hatenablog.com