🙆

CDKでデプロイしたAppRunnerが自動デプロイできない問題

2023/08/04に公開

先日、CDKでデプロイしたAppRunnerが自動デプロイが適用になっているのに反映されない問題が発生したので暫定対応をまとめておきます。

実行環境は以下です。

  • aws-cdk-lib: 2.89.0
  • @aws-cdk/aws-apprunner-alpha: 2.89.0-alpha.0

AppRunnerのデプロイ構成としては、ECRのコンテナイメージをソースとしてイメージプッシュ毎にサービスをデプロイする構成です。

自動デプロイが効かない時

import * as apprunner from '@aws-cdk/aws-apprunner-alpha'
import { Cpu, Memory } from '@aws-cdk/aws-apprunner-alpha'
import * as cdk from 'aws-cdk-lib'
import * as iam from 'aws-cdk-lib/aws-iam'
import { Construct } from 'constructs'
import { EcrStack } from './ecr'

export class AppRunnerStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    ecrStack: EcrStack,
    props?: cdk.StackProps,
  ) {
    super(scope, id, props)

    new apprunner.Service(this, 'SampleAppRunnerService', {
      serviceName: 'sample-app',
      cpu: Cpu.ONE_VCPU,
      memory: Memory.TWO_GB,
      autoDeploymentsEnabled: true,
      source: apprunner.Source.fromEcr({
        imageConfiguration: {
          port: 3000,
          startCommand: 'npm run start --workspace=app',
        },
        repository: ecrStack.repository,
        tagOrDigest: 'latest',
      }),
    })
  }
}

ECRの定義

import * as cdk from 'aws-cdk-lib'
import * as ecr from 'aws-cdk-lib/aws-ecr'
import { Construct } from 'constructs'

export class EcrStack extends cdk.Stack {
  public readonly repository: ecr.Repository
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    this.repository = new ecr.Repository(this, 'SampleAppRepository', {
      repositoryName: 'sample-app-repository',
    })
  }
}

デプロイ自体は問題なく、コンソールからAppRunnerの設定値を見ると「自動デプロイ」になっていますが、ECRのリポジトリにDockerイメージをプッシュしてもAppRunnerのデプロイが動かない状況でした。

原因

上記のコードの場合、AppRunner側のサービスのアクセスロールが自動で生成されるのですが、アクション ecr:DescribeImages の許可が漏れていてイメージの変更を検知できていないようでした。

対応

対策としてはシンプルにIAMロールを自前で作成して、アクセスロールに設定する必要があります。

リソースを絞る必要がなければ、AWSの管理ロールを使うのが手っ取り早いです。

import * as apprunner from '@aws-cdk/aws-apprunner-alpha'
import { Cpu, Memory } from '@aws-cdk/aws-apprunner-alpha'
import * as cdk from 'aws-cdk-lib'
import * as iam from 'aws-cdk-lib/aws-iam'
import { Construct } from 'constructs'
import { EcrStack } from './ecr'

export class AppRunnerStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    ecrStack: EcrStack,
    props?: cdk.StackProps,
  ) {
    super(scope, id, props)

    new apprunner.Service(this, 'SampleAppRunnerService', {
      serviceName: 'sample-app',
      cpu: Cpu.ONE_VCPU,
      memory: Memory.TWO_GB,
      autoDeploymentsEnabled: true,
+      accessRole: new iam.Role(this, 'SampleAppRunnerAccessRole', {
+        roleName: 'SampleAppRunnerAccessRole',
+        assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
+        managedPolicies: [
+          iam.ManagedPolicy.fromAwsManagedPolicyName(
+            'AWSAppRunnerServicePolicyForECRAccess',
+          ),
+        ],
+      }),
      source: apprunner.Source.fromEcr({
        imageConfiguration: {
          port: 3000,
          startCommand: 'npm run start --workspace=app',
        },
        repository: ecrStack.repository,
        tagOrDigest: 'latest',
      }),
    })
  }
}

リソースを対象リポジトリに絞る場合は以下のように定義します。

import * as apprunner from '@aws-cdk/aws-apprunner-alpha'
import { Cpu, Memory } from '@aws-cdk/aws-apprunner-alpha'
import * as cdk from 'aws-cdk-lib'
import * as iam from 'aws-cdk-lib/aws-iam'
import { Construct } from 'constructs'
import { EcrStack } from './ecr'

export class AppRunnerStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    ecrStack: EcrStack
    props?: cdk.StackProps,
  ) {
    super(scope, id, props)

+    const accessRole = new iam.Role(this, 'SampleAppRunnerAccessRole', {
+      roleName: 'SampleAppRunnerAccessRole',
+      assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
+      inlinePolicies: {
+        SampleAppRunnerAccessRole: new iam.PolicyDocument({
+          statements: [
+            new iam.PolicyStatement({
+              effect: iam.Effect.ALLOW,
+              actions: ['ecr:GetAuthorizationToken'],
+              resources: ['*'],
+            }),
+            new iam.PolicyStatement({
+              effect: iam.Effect.ALLOW,
+              actions: [
+                'ecr:BatchCheckLayerAvailability',
+                'ecr:BatchGetImage',
+                'ecr:DescribeImages',
+                'ecr:GetDownloadUrlForLayer',
+              ],
+              resources: [ecrStack.repository.repositoryArn],
+            }),
+          ],
+        }),
+      },
+    })

    new apprunner.Service(this, 'SampleAppRunnerService', {
      serviceName: 'sample-app',
      cpu: Cpu.ONE_VCPU,
      memory: Memory.TWO_GB,
      autoDeploymentsEnabled: true,
+      accessRole,
      source: apprunner.Source.fromEcr({
        imageConfiguration: {
          port: 3000,
          startCommand: 'npm run start --workspace=app',
        },
        repository: ecrStack.repository,
        tagOrDigest: 'latest',
      }),
    })
  }
}

Discussion