こんにちは。今年8月に発足したラクスル事業本部 HoE(※)室 の市島です。
※ HoE:Head of Engineering
はじめに
以前公開した「Permissions Boundaryでガードレールを設けてAWS CDKを安全に使う」という記事の中でも紹介していますが、HoE室ではAWS CDKとPermissions Boundaryの組み合わせで、高速かつ安全にサーバーレスアプリを開発しています。
ラクスルではSecrets Managerに登録する秘匿情報やKMS、S3のバケットはインフラチームによって別管理されています。また、CDKはTypeScriptで書いています。
上記の前提で、開発中にハマってしまった点を3つほど紹介できればと思います。
権限の付与忘れがち
CDKでは利用するAWSのサービスに対してよしなに権限を与えてくれるので非常に便利なのですが、こちらから明示的に権限を与えてやらないと動かない場面があったりします。
例えばLambda内でSecrets ManagerからAPI Tokenなどの秘匿情報を取得するとなった場合、以下のようにSecrets ManagerへのRead権限を与えることが必要になります。
const secret = secretsmanager.Secret.fromSecretNameV2(this, 'Secret', 'secret') secret.grantRead(lambdaFn)
secretsのdecodeにはKMSへのアクセス権限も必要になるため実際これだけでは動かず、以下のようにKMSへの権限も別途付与する必要があります。
const kmsPolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["kms:Decrypt"], resources: ["*"], }) lambdaFn.addToRolePolicy(kmsPolicy) const secret = secretsmanager.Secret.fromSecretNameV2(this, 'Secret', 'secret') secret.grantRead(lambdaFn)
またLambda内で既存のS3のバケットにアクセスするには、バケットに対してLambdaが読み書きできる権限が必要でした。
const bucket = s3.Bucket.fromBucketName( this, 'bucketName', 'bucketName' ) bucket.grantReadWrite(lambdaFn)
Docker Hubのrate limit引っかかりがち
CD(Continuous Delivery)にはCodePipelineを使っており、CodeBuildにてgolangで書かれたLambdaのdocker imageをbuildするプロセスがあります。
このプロセスでDocker Hubからimageをpullしてくる際にrate limitに引っかかることがありました。
対策としてはdocker loginするのが手っ取り早いです。既に社内のdockerアカウントは存在していたので、そのアカウントでのdocker loginの処理を追加しました。
const dockerHubSecret = secretsmanager.Secret.fromSecretNameV2(this, 'dockerHubSecret', 'dockerhub'); const kmsPolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["kms:Decrypt"], resources: ["*"], }) const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { dockerCredentials: [ pipelines.DockerCredential.dockerHub(dockerHubSecret) ], codeBuildDefaults: { rolePolicy: [kmsPolicy] }, ...
しかしこれでもrate limitにひっかかることがありました。
調べてみると、記事執筆時点ではワークアラウンドな対策しかありませんでした。(issue #15737)
そのため、今はdockerCredentials
を以下のように記述することでrate limitに引っかからないようにしています。
dockerCredentials: [ pipelines.DockerCredential.dockerHub(dockerHubSecret), pipelines.DockerCredential.customRegistry("<https://index.docker.io/v1/>", dockerHubSecret) ],
マルチアカウントでの運用方法を迷いがち
CDKを使ってアプリケーションを構築する際、本番環境・開発環境など複数のAWSアカウントに対してデプロイをすることが多いでしょう。このとき、アカウント毎にリソースの定義や振る舞いを変えたい場合があります。私たちのアプリケーションでは、参照するAWSリソースやLambdaの振る舞いをアカウント毎に変える必要がありました。
これを実現する方法は、以下の2つが考えられます。
- アカウント毎に別々のStackを作る
- Contextを使って依存性を注入する
「アカウント毎に別々のStackを作る」場合には、それぞれのStackにアカウント固有の情報をハードコードすることで、アカウント毎にリソース定義を変えられます。
「Contextを使って依存性を注入する」場合には、CDKのContextを使います。Contextは、StackやConstructに渡すことができるkey-valueペアで、cdk.json
のような設定ファイルやcdk deploy
やcdk synth
のオプションから与えることができます。
今回は「Contextを使って依存性を注入する」方法を採用しました。まずcdk.jsonに以下のような設定を追加します。
"context": { "accounts": { "dev": { "arn": "xxxxxx" }, "prod": { "arn": "yyyyyy" } },
さらにcdk deploy
やcdk synth
の際に、env
というcontextを設定するようにします。
$ cdk deploy -c env=dev # 開発環境にデプロイ $ cdk deploy -c env=prod # 本番環境にデプロイ
これらのContextを読み込む処理をStackに追加します。まず、env
の値を読み込みます。この時、値が設定されていない場合には終了するようにしています。
const env = scope.node.tryGetContext('env') if (env == undefined) { console.log('"env" must be set to context.') process.exit(1) }
次に、cdk.json
に定義した値を読み込みます。まずaccounts
を読み込み、そこからenv
に対応する値を取り出します。
const accounts = scope.node.tryGetContext('accounts'); const accountConfig = accounts[env]
例えばenv=dev
とした場合には、accountConfig
に入る値は以下のようになります。
{ "arn": "xxxxxx" }
ここから各種設定値を取り出します (実際には複数の設定値を取り出しています)。
const arn = accountConfig['arn'];
あとはCDKの定義にこの定数を使えば、アカウント毎にカスタマイズしたリソースを定義することができます。
また以下のようにLambdaの環境変数にcontextから読んだ値を渡すことで、依存性注入が可能です。
const sampleFn = new lambda.DockerImageFunction( this, 'sample', { functionName: 'sample', code: lambda.DockerImageCode.fromImageAsset( path.join(__dirname, '../../lambda/sample'), {} ), environment: { APP_ENV: env, }, } )
Lambdaのアプリケーションコードにて、環境変数APP_ENV
によって処理を書き分けることで、アカウント毎にLambdaの振る舞いを変えることができます。
おわりに
AWS CDKを用いると、Cloudformationのテンプレートを長々書かなくてもよくなり、普段慣れ親しんだプログラミング言語で型補完を効かせながらスイスイ書くことができます。
CDKはよしなにやってくれるので非常に助かる反面、意識していないとハマるポイントもあります。サービス自体が比較的新しく現時点ではそこまで情報が多いわけではないので、この記事が何かのお役に立てば幸いです。