AppSyncのログ・WAF・X-Ray設定をCDKで行ってみる

2022.05.02

吉川です。

早速ですが、AppSyncを使う上でよく必要になると思われる設定をCDKで行ってみたので紹介します。

  • ログをCloudWatch Logsに出力
  • AWS WAFの設定と紐付け
  • X-Ray設定

上記を行っていきます。

初期コード

下記のStack定義CDKコードを用意します。Lambda関数をDataSourceとするシンプルなAppSyncAPIを定義しました。

lib/appsync-stack.ts

import { Construct } from 'constructs'
import * as cdk from 'aws-cdk-lib'
import * as appsync from '@aws-cdk/aws-appsync-alpha'
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'

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

    // AppSyncAPIを作成
    const api = new appsync.GraphqlApi(this, 'myAppsyncApi', {
      name: 'myAppsyncApi',
      schema: appsync.Schema.fromAsset('./schema.graphql'),
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.API_KEY,
        },
      },
    })


    // Lambda関数を作成
    const findUserFn = new lambdaNodejs.NodejsFunction(this, 'findUserFn', {
      entry: 'lambda-handler/find-user-handler.ts',
    })

    // Lambda関数をDataSourceとしてAppSyncAPIと紐付ける
    const findUserDs = api.addLambdaDataSource('findUserDs', findUserFn)

    // schema.graphqlで定義した中のどの操作とマッピングするかを指定
    findUserDs.createResolver({
      typeName: 'Query',
      fieldName: 'findUser',
    })
  }
}

Stack定義以外のファイルは以下です。

bin/cdk.ts

#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { AppSyncStack } from '../lib/appsync-stack'

const app = new cdk.App()
new AppSyncStack(app, 'AppSyncStack', {
  env: { region: 'ap-northeast-1' },
})

lambda-handler/find-user-handler.ts

import { AppSyncResolverEvent } from 'aws-lambda'

export const handler = async (event: AppSyncResolverEvent<{}, {}>) => {
  console.log(JSON.stringify({ event }))

  return {
    id: '1',
    name: 'John Doe',
  }
}

schema.graphql

type User {
  id: String!
  name: String!
}
type Query {
  findUser: User!
}

ログをCloudWatch Logsに出力する

まずはAppSyncログをCloudWatch Logsに出力する設定を追加します。

lib/appsync-stack.ts

    // ログ出力のためにAppSyncにアタッチするIAM Role作成
    const apiLogRole = new iam.Role(this, 'apiLogRole', {
      assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'),
      path: '/service-role/',
    })
    apiLogRole.attachInlinePolicy(
      new iam.Policy(this, 'apiLogRolePolicy', {
        statements: [
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'logs:CreateLogGroup',
              'logs:CreateLogStream',
              'logs:PutLogEvents',
            ],
            resources: [`arn:aws:logs:${this.region}:${this.account}:*`],
          }),
        ],
      })
    )

    // AppSyncAPIを作成
    const api = new appsync.GraphqlApi(this, 'myAppsyncApi', {
      name: 'myAppsyncApi',
      schema: appsync.Schema.fromAsset('./schema.graphql'),
      logConfig: {
        role: apiLogRole,
        fieldLogLevel: appsync.FieldLogLevel.ERROR,
      },
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.API_KEY,
        },
      },
    })

ログ出力用のIAMロールを作成し、 logConfig にロールとエラーレベルを渡すことで設定できました。

AWS WAFの設定と紐付け

続いてAWS WAFのWebACLを作成し、それをAppSyncに紐付けます。

[AWS CDK] WAF v2のAWSマネージドルールをCloudFrontに適用する | DevelopersIO

こちらを参考にして

  • AWSManagedRulesCommonRuleSet
  • AWSManagedRulesAdminProtectionRuleSet
  • AWSManagedRulesKnownBadInputsRuleSet
  • AWSManagedRulesAmazonIpReputationList
  • AWSManagedRulesAnonymousIpList

をルールとして設定しました。

lib/appsync-stack.ts

    // AppSync用のWebACLを作成
    const apiWebAcl = new wafV2.CfnWebACL(this, 'wafV2WebAcl', {
      defaultAction: { allow: {} },
      // 'CLOUDFRONT' or 'REGIONAL'
      // AppSyncは'REGIONAL'を選択
      scope: 'REGIONAL',
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: 'wafV2WebAclMetric',
      },
      rules: [
        {
          name: 'AWSManagedRulesCommonRuleSet',
          priority: 1,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesCommonRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesCommonRuleSet',
          },
        },
        {
          name: 'AWSManagedRulesAdminProtectionRuleSet',
          priority: 2,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesAdminProtectionRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesAdminProtectionRuleSet',
          },
        },
        {
          name: 'AWSManagedRulesKnownBadInputsRuleSet',
          priority: 3,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesKnownBadInputsRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesKnownBadInputsRuleSet',
          },
        },
        {
          name: 'AWSManagedRulesAmazonIpReputationList',
          priority: 4,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesAmazonIpReputationList',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesAmazonIpReputationList',
          },
        },
        {
          name: 'AWSManagedRulesAnonymousIpList',
          priority: 5,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesAnonymousIpList',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesAnonymousIpList',
          },
        },
      ],
    })

    // AppSyncとWebACLを紐付ける
    const webAclAssociation = new wafV2.CfnWebACLAssociation(this, 'webAclAssociation', {
      resourceArn: api.arn,
      webAclArn: apiWebAcl.attrArn,
    })
    // 本リソースはAppSyncとWebACLより後に設定されなければならないため、DependsOnに両者を設定
    webAclAssociation.addDependsOn(apiWebAcl)
    webAclAssociation.addDependsOn(api.node.defaultChild as cdk.CfnResource)

X-Ray設定

最後にX-Rayを有効化します。これは xrayEnabled: true を指定するだけで実現できます。

lib/appsync-stack.ts

    // AppSyncAPIを作成
    const api = new appsync.GraphqlApi(this, 'myAppsyncApi', {
      name: 'myAppsyncApi',
      schema: appsync.Schema.fromAsset('./schema.graphql'),
      logConfig: {
        role: apiLogRole,
        fieldLogLevel: appsync.FieldLogLevel.ERROR,
      },
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.API_KEY,
        },
      },
      xrayEnabled: true,
    })

完成コード

lib/appsync-stack.ts

import { Construct } from 'constructs'
import * as cdk from 'aws-cdk-lib'
import * as appsync from '@aws-cdk/aws-appsync-alpha'
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'
import * as logs from 'aws-cdk-lib/aws-logs'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as wafV2 from 'aws-cdk-lib/aws-wafv2'

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

    // ログ出力のためにAppSyncにアタッチするIAM Role作成
    const apiLogRole = new iam.Role(this, 'apiLogRole', {
      assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'),
      path: '/service-role/',
    })
    apiLogRole.attachInlinePolicy(
      new iam.Policy(this, 'apiLogRolePolicy', {
        statements: [
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              'logs:CreateLogGroup',
              'logs:CreateLogStream',
              'logs:PutLogEvents',
            ],
            resources: [`arn:aws:logs:${this.region}:${this.account}:*`],
          }),
        ],
      })
    )

    // AppSyncAPIを作成
    const api = new appsync.GraphqlApi(this, 'myAppsyncApi', {
      name: 'myAppsyncApi',
      schema: appsync.Schema.fromAsset('./schema.graphql'),
      logConfig: {
        role: apiLogRole,
        fieldLogLevel: appsync.FieldLogLevel.ERROR,
      },
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.API_KEY,
        },
      },
      xrayEnabled: true,
    })

    // AppSync用のCloudWatch LogGroup作成
    new logs.LogGroup(this, 'apiLogGroup', {
      logGroupName: `/aws/appsync/apis/${api.apiId}`,
    })

    // AppSync用のWebACLを作成
    const apiWebAcl = new wafV2.CfnWebACL(this, 'wafV2WebAcl', {
      defaultAction: { allow: {} },
      // 'CLOUDFRONT' or 'REGIONAL'
      // AppSyncは'REGIONAL'を選択
      scope: 'REGIONAL',
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: 'wafV2WebAclMetric',
      },
      rules: [
        {
          name: 'AWSManagedRulesCommonRuleSet',
          priority: 1,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesCommonRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesCommonRuleSet',
          },
        },
        {
          name: 'AWSManagedRulesAdminProtectionRuleSet',
          priority: 2,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesAdminProtectionRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesAdminProtectionRuleSet',
          },
        },
        {
          name: 'AWSManagedRulesKnownBadInputsRuleSet',
          priority: 3,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesKnownBadInputsRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesKnownBadInputsRuleSet',
          },
        },
        {
          name: 'AWSManagedRulesAmazonIpReputationList',
          priority: 4,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesAmazonIpReputationList',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesAmazonIpReputationList',
          },
        },
        {
          name: 'AWSManagedRulesAnonymousIpList',
          priority: 5,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesAnonymousIpList',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'AWSManagedRulesAnonymousIpList',
          },
        },
      ],
    })

    // AppSyncとWebACLを紐付ける
    const webAclAssociation = new wafV2.CfnWebACLAssociation(this, 'webAclAssociation', {
      resourceArn: api.arn,
      webAclArn: apiWebAcl.attrArn,
    })
    // 本リソースはAppSyncとWebACLより後に設定されなければならないため、DependsOnに両者を設定
    webAclAssociation.addDependsOn(apiWebAcl)
    webAclAssociation.addDependsOn(api.node.defaultChild as cdk.CfnResource)

    // Lambda関数を作成
    const findUserFn = new lambdaNodejs.NodejsFunction(this, 'findUserFn', {
      entry: 'lambda-handler/find-user-handler.ts',
    })

    // Lambda関数をDataSourceとしてAppSyncAPIと紐付ける
    const findUserDs = api.addLambdaDataSource('findUserDs', findUserFn)

    // schema.graphqlで定義した中のどの操作とマッピングするかを指定
    findUserDs.createResolver({
      typeName: 'Query',
      fieldName: 'findUser',
    })
  }
}

確認

npx cdk deploy でデプロイし、作成されたAppSyncリソースの設定をマネジメントコンソールで開くと、

このようにひと通りの設定がオンになっていることを確認できました。

参考