Upgrade to Pro — share decks privately, control downloads, hide ads and more …

サーバーレスパターンを元にAWS CDKでデータ基盤を構築する / 20240731_clas...

サーバーレスパターンを元にAWS CDKでデータ基盤を構築する / 20240731_classmethod_odyssey_online_build_a_data_infrastructures_using_aws_cdk_based_on_serverless_patterns

2024/07/31 Classmethod Odyssey オンラインにて発表した資料。

Classmethod Odyssey:
https://classmethod.jp/m/odyssey/

DevelopersIO article:
https://dev.classmethod.jp/articles/building-data-infrastructures-with-aws-cdk-based-on-the-serverless-pattern/

kasacchiful

July 31, 2024
Tweet

More Decks by kasacchiful

Other Decks in Programming

Transcript

  1. ⾃⼰紹介 l 笠原 宏 l データ事業本部ビジネスソリューション部SAチーム ソリューションアーキテクト l 新潟県新潟市在住 l

    JAWS-UG新潟⽀部 / Python機械学習勉強会in新潟 / JaSST新潟 / SWANII / Cloudflare Meetup Niigata l AWS Community Builder / 2024 Japan AWS All Certifications Engineer l 好きなAWSサービス: S3 / Lambda / Step Functions 3 @kasacchiful @kasacchiful
  2. 今回の構成 14 l CDK: TypeScript l Lambda: Python l CDKは、サンプルコードが多い

    TypeScriptが理解しやすく扱いやすい l Lambdaは、Pythonがデータを扱いやすく、 Glueへの転⽤もしやすい l CDKで使うライブラリとLambdaで使うライブラリを 分けて管理しやすい
  3. CDKでのファイル構成 20 ├── README.md ├── bin │ └── serverless-pattern-on-cdk.ts ├──

    cdk.json ├── env ├── jest.config.js ├── lib │ └── simple-s3-data-processing-stack.ts ## Stack: SimpleS3DataProcessingStack の定義 ├── node_modules ├── package-lock.json ├── package.json ├── requirements.txt ├── resources │ └── simple-s3-data-processing ## Stack: SimpleS3DataProcessingStack のLambda関数リソース │ └── lambda │ └── data-processing.py ├── test └── tsconfig.json l 今回はサンプルコード毎に CDKスタックを分けている l Lambda関数のコードは resources ディレクトリ内に格納
  4. スタック定義 (S3) 21 // S3 Buckets const sourceBucket = new

    Bucket(this, 'SourceBucket', { bucketName: `${props?.projectName}-source-bucket`, removalPolicy: cdk.RemovalPolicy.RETAIN, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, encryption: BucketEncryption.S3_MANAGED, }); const destinationBucket = new Bucket(this, 'DestinationBucket', { bucketName: `${props?.projectName}-destination-bucket`, removalPolicy: cdk.RemovalPolicy.RETAIN, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, encryption: BucketEncryption.S3_MANAGED, }); l バケットを2つ定義
  5. スタック定義 (IAM Role) 22 FullAccessDestinationBucket: new PolicyDocument({ statements: [new PolicyStatement({

    effect: Effect.ALLOW, actions: [ 's3:*', ], resources: [ destinationBucket.bucketArn, `${destinationBucket.bucketArn}/*`, ], })] }), } }); lambdaRole.addManagedPolicy( cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName( 'service-role/AWSLambdaBasicExecutionRole' )); // IAM Role for Lambda const lambdaRole = new Role(this, 'LambdaRole', { roleName: `${props?.projectName}-lambda-role`, assumedBy: new ServicePrincipal('lambda.amazonaws.com'), inlinePolicies: { ReadOnlySourceBucket: new PolicyDocument({ statements: [new PolicyStatement({ effect: Effect.ALLOW, actions: [ 's3:Get*', 's3:List*', ], resources: [ sourceBucket.bucketArn, `${sourceBucket.bucketArn}/*`, ], })] }), l LambdaにアタッチするIAMロールを定義
  6. スタック定義 (Lambda Layer) 23 // Lambda for Data Processing const

    awsSdkForPandasLayer = LayerVersion.fromLayerVersionArn( this, 'AwsSdkForPandasLayer', 'arn:aws:lambda:ap-northeast-1:336392948345:layer:AWSSDKPandas-Python312- Arm64:12' ); l AWS SDK for Pandasを利⽤したいので、 今回はManagedで⽤意されているLambda Layerを利⽤する l 詳細はAWS SDK for Pandasのドキュメントを参照 https://aws-sdk-pandas.readthedocs.io/en/stable/install.html
  7. スタック定義 (Lambda Function) 24 const dataProcessingFunction = new Function(this, 'DataProcessingFunction',

    { functionName: `${props?.projectName}-data-processing-function`, runtime: Runtime.PYTHON_3_12, architecture: Architecture.ARM_64, timeout: cdk.Duration.seconds(30), memorySize: 512, handler: 'data-processing.lambda_handler', code: Code.fromAsset('./resources/simple-s3-data-processing/lambda/'), role: lambdaRole, layers: [ awsSdkForPandasLayer, ], environment: { 'DESTINATION_BUCKET_NAME': destinationBucket.bucketName, }, });
  8. スタック定義 (イベントソース) 25 // add event source. dataProcessingFunction.addEventSource( new cdk.aws_lambda_event_sources.S3EventSourceV2(sourceBucket,

    { events: [EventType.OBJECT_CREATED,], })); l Lambda関数のトリガーを設定 l 今回はソースバケットにファイルが置かれた際に起動するようにした
  9. Lambda関数コード 26 import awswrangler as wr def lambda_handler(event, context): ##

    <省略> ## to parquet (ex: sample.csv => sample.parquet) sourcePath = f's3://{os.path.join(sourceBucket, sourceObject)}' destinationPath = f's3://{os.path.join(destinationBucket, os.path.dirname(sourceObject))}{os.path.splitext(os.path.basename(sourceObject))[0]}.parquet' df = wr.s3.read_csv(sourcePath, dtype_backend='pyarrow') wr.s3.to_parquet(df, path=destinationPath, index=False) ## <省略> l AWS SDK for Pandasを使って、 ソースバケットにあるCSVファイルを読み込み、 デスティネーションバケットにParquetファイルを書き出す
  10. デプロイ 27 ## Cloudformationテンプレート確認 npx cdk synth SimpleS3DataProcessingStack ## 変更内容の比較

    npx cdk diff SimpleS3DataProcessingStack ## デプロイ npx cdk deploy SimpleS3DataProcessingStack
  11. CDKでのファイル構成 35 ├── README.md ├── bin │ └── serverless-pattern-on-cdk.ts ├──

    cdk.json ├── env ├── jest.config.js ├── lib │ └── event-driven-collaboration-stack.ts ## Stack: EventDrivenCollaborationStack の定義 ├── node_modules ├── package-lock.json ├── package.json ├── requirements.txt ├── resources │ └── event-driven-collaboration-stack ## Stack: EventDrivenCollaborationStack のLambda関数リソース │ └── lambda │ ├── t-_change_format.py │ └── t-_change_type.py ├── test └── tsconfig.json l 今回はサンプルコード毎に CDKスタックを分けている l Lambda関数のコードは resources ディレクトリ内に格納
  12. スタック定義 (SNS‧SQS) 36 // SNS const snsTopic = new Topic(this,

    'SnsTopic', { topicName: `${props?.projectName}-sns-topic.fifo`, fifo: true, contentBasedDeduplication: false, }); // SQS const sqs_queue = new Queue(this, 'SqsQueue', { queueName: `${props?.projectName}-sqs-queue.fifo`, fifo: true, contentBasedDeduplication: false, }); snsTopic.addSubscription( new SqsSubscription(sqs_queue, { rawMessageDelivery: true, })); l FIFOトピックとFIFOキューを定義 l トピックのサブスクリプション先は キューとなる
  13. スタック定義 (SNS‧SQS) 37 sqs_queue.addToResourcePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'sqs:*',

    ], resources: [ sqs_queue.queueArn, ], principals: [ new AccountRootPrincipal, ], })); l キューのリソースポリシーには、 SNSトピックからの「SendMessage」 およびLambda関数への 「ReceiveMessage」「DeleteMessage」は CDKが付与してくれる l 今回はポリシー定義例として、 AWSアカウントでのキュー操作を 全て許可する設定を追加している
  14. スタック定義 (Lambda関数1) 38 ## <省略> ## to parquet (ex: sample.csv

    => sample.parquet) sourcePath = f's3://{os.path.join(sourceBucket, sourceObject)}' destinationPath = f's3://{os.path.join(destinationBucket, os.path.dirname(sourceObject))}{os.path.splitext(os.path.basename(sourceObject))[0]}.parquet' df = wr.s3.read_csv(sourcePath, dtype_backend='pyarrow') wr.s3.to_parquet(df, path=destinationPath, index=False) sns = boto3.client('sns') sns.publish( TopicArn=snsTopicArn, Message=json.dumps({ 'bucket_name': destinationBucket, 'object_key': f'{os.path.dirname(sourceObject)}{os.path.splitext(os.path.basename(sourceObject))[0]}.parquet' }), MessageDeduplicationId=str(uuid.uuid4()), MessageGroupId='toChangeFormat', ) l 最初の処理では、Parquetファイルに変換後、 SNSトピックにメッセージを送信している l 重複排除として、今回は MessageDedupulicationIdと MessageGroupIdを設定する⽅法を⽤いている
  15. スタック定義 (Lambda関数2) 39 def lambda_handler(event, context): msg = json.loads(event['Records'][0].get('body', {}))

    ## <省略> ## to change type (ex: column: date, string => date) sourcePath = f's3://{os.path.join(sourceBucket, sourceObject)}' destinationPath = f's3://{os.path.join(destinationBucket, os.path.dirname(sourceObject))}{os.path.basename(sourceObject)}' df = wr.s3.read_parquet(sourcePath) df = df.astype({'date': 'date64[pyarrow]'}) wr.s3.to_parquet(df, path=destinationPath, index=False) l 次の処理では、キューのポーリング実装不要で キューのメッセージを取得してから ⼀部のカラムの型を変更する処理を⼊れている
  16. デプロイ 40 ## Cloudformationテンプレート確認 npx cdk synth EventDrivenCollaborationStack ## 変更内容の比較

    npx cdk diff EventDrivenCollaborationStack ## デプロイ npx cdk deploy EventDrivenCollaborationStack
  17. ETLフローをステートマシンで制御 45 l Lambda / Glue / ECS / EKS

    / Batch な どのリソース使って ETLフロー制御 l 並列実⾏制御も容易
  18. CDKでのファイル構成 52 ├── README.md ├── bin │ └── serverless-pattern-on-cdk.ts ├──

    cdk.json ├── env ├── jest.config.js ├── lib │ ├── state-machine-chain │ │ ├── common │ │ │ └── s3.ts │ │ ├── first-processing │ │ │ ├── lambda.ts │ │ │ └── step-functions.ts │ │ ├── inter-processing │ │ │ └── sns-sqs.ts │ │ └── second-processing │ │ │ ├── lambda.ts │ │ │ └── step-functions.ts │ └── state-machine-chain-stack.ts ## Stack: StateMachineChainStack ...(省略)... ├── resources │ └── state-machine-chain-stack │ ├── first-processing │ │ ├── lambda │ │ │ └── to-change-format.py │ │ └── step-functions │ │ └── first-processing-state-machine.asl.yaml │ └── second-processing │ ├── lambda │ │ └── to-change-type.py │ └── step-functions │ └── second-processing-state-machine.asl.yaml ...(省略)... l 今回はサンプルコード毎に CDKスタックを分けている l Lambda関数のコードは resources ディレクトリ内に格納 l 処理毎にリソースを分けた
  19. スタック定義 (Step Functions1) 53 // State Machine this.stateMachine = new

    StateMachine(this, 'FirstProcessingStateMachine', { stateMachineName: `${props.projectName}-first-processing-state-machine`, role: stateMachineRole, definitionBody: DefinitionBody.fromFile( 'resources/state-machine-chain/first-processing/step-functions/first-processing-state-machine.asl.yaml' ), definitionSubstitutions: { ToChangeFormatFunctionArn: props.toChangeFormatFunction.functionArn, SnsTopicArn: props.snsTopic.topicArn, } }); // event schedule rule new Rule(this, 'FirstProcessStateMachineScheduleRule', { ruleName: `${props.projectName}-first-state-machine-rule`, schedule: Schedule.cron({ minute: '0', hour: '21', }), targets: [new SfnStateMachine(this.stateMachine)], enabled: false, }); l ステートマシンはASL YAMLで定義
  20. スタック定義 (Lambda関数1) 55 def lambda_handler(event, context): sourceBucket = os.environ.get('SOURCE_BUCKET_NAME') destinationBucket

    = os.environ.get('DESTINATION_BUCKET_NAME') destinationPaths = [] s3 = boto3.resource('s3') obj_list = [ k.key for k in s3.Bucket(sourceBucket).objects.all() if (os.path.splitext(os.path.basename(k.key))[1] == '.csv')] for sourceObject in obj_list: ## to parquet (ex: sample.csv => sample.parquet) sourcePath = f's3://{os.path.join(sourceBucket, sourceObject)}' destinationPath = f's3://{os.path.join(destinationBucket, os.path.dirname(sourceObject), os.path.splitext(os.path.basename(sourceObject))[0])}.parquet' df = wr.s3.read_csv(sourcePath, dtype_backend='pyarrow') wr.s3.to_parquet(df, path=destinationPath, index=False) destinationPaths.append({"bucket": destinationBucket, "path": destinationPath}) return { 'code': 200, 'msg': { 'destination': destinationPaths }, } l SNSトピックへの送信は ステートマシンに任せる
  21. スタック定義 (Step Functions2) 56 // State Machine this.stateMachine = new

    StateMachine(this, 'SecondProcessingStateMachine', { stateMachineName: `${props.projectName}-second-processing-state-machine`, role: stateMachineRole, definitionBody: DefinitionBody.fromFile( 'resources/state-machine-chain/second-processing/step-functions/second-processing-state-machine.asl.yaml' ), definitionSubstitutions: { ToChangeTypeFunctionArn: props.toChangeTypeFunction.functionArn, } }); // Pipe new CfnPipe(this, 'Pipe', { source: props.sqsQueue.queueArn, sourceParameters: { sqsQueueParameters: { batchSize: 1, }, }, target: this.stateMachine.stateMachineArn, targetParameters: { stepFunctionStateMachineParameters: { invocationType: 'FIRE_AND_FORGET’, } // Invoke asynchronously }, roleArn: pipeRunnerRole.roleArn }); l EventBridge Pipesは、 L1コンストラクトで定義 l v2.150.0時点ではL2は まだ⽤意されていない
  22. スタック定義 (Lambda関数2) 58 def lambda_handler(event, context): sourceBucket = event.get('bucket') sourcePath

    = event.get('path') sourceObject = re.match(f'^s3://{sourceBucket}/(.+)', sourcePath).group(1) destinationBucket = os.environ.get('DESTINATION_BUCKET_NAME') ## to change type (ex: column: date, string => date) destinationPath = f's3://{os.path.join(destinationBucket, os.path.dirname(sourceObject), os.path.basename(sourceObject))}' df = wr.s3.read_parquet(sourcePath) df = df.astype({'date': 'date64[pyarrow]'}) wr.s3.to_parquet(df, path=destinationPath, index=False) return { 'code': 200, 'msg': { 'destination': destinationPath }, } l 変換対象が複数あっても、 ステートマシンのMapによって 渡されるs3パスは1つだけ
  23. デプロイ 59 ## Cloudformationテンプレート確認 npx cdk synth StateMachineChainStack ## 変更内容の比較

    npx cdk diff StateMachineChainStack ## デプロイ npx cdk deploy StateMachineChainStack
  24. コンピュートリソースを考える 62 l Lambda以外にも、Glue / ECS / EKS / Batch

    / EMR などのコンピュートリソース を使うこともある l 必要に応じて使い分けておくとよい
  25. ざっと⽐較 63 サービス名 起動速度 処理速度 スケール 速度 処理時間 開発 しやすさ

    管理 しやすさ Lambda ◎ ◯ ◎ △ ◎ ◎ Glue △ ◎ △ ◎ ◯ ◯ ECS/EKS ◯ ◯ △ ◎ ◎ ◯ Batch △ ◎ ◯ ◎ ◯ ◯ EMR △ ◎ △ ◎ ◯ △ l どのコンピュートリソースを使用したら良いか、検討すること l 処理毎にコンピュートリソースを変えることも検討すること ※個人主観込
  26. 処理毎に配置する 65 ├── README.md ├── bin │ └── serverless-pattern-on-cdk.ts ├──

    cdk.json ├── env ├── jest.config.js ├── lib │ ├── state-machine-chain │ │ ├── common │ │ │ └── s3.ts │ │ ├── first-processing │ │ │ ├── lambda.ts │ │ │ └── step-functions.ts │ │ ├── inter-processing │ │ │ └── sns-sqs.ts │ │ └── second-processing │ │ │ ├── lambda.ts │ │ │ └── step-functions.ts │ └── state-machine-chain-stack.ts ## Stack: StateMachineChainStack ...(省略)... ├── resources │ └── state-machine-chain-stack │ ├── first-processing │ │ ├── lambda │ │ │ └── to-change-format.py │ │ └── step-functions │ │ └── first-processing-state-machine.asl.yaml │ └── second-processing │ ├── lambda │ │ └── to-change-type.py │ └── step-functions │ └── second-processing-state-machine.asl.yaml ...(省略)... l 規模が⼤きくなれば、 処理毎にファイルを分けて 配置した⽅が⾒通しが良くなる
  27. 環境毎のパラメータ設定 66 import { Environment } from "aws-cdk-lib"; export interface

    AppParameter { env: Environment; envName: string; projectName: string; } export const devParameter: AppParameter = { envName: "dev", projectName: "sls-patterns", env: {}, }; export const prodParameter: AppParameter = { envName: "prod", projectName: "sls-patterns", env: {}, }; #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { devParameter, prodParameter } from "../env_parameters"; import { SlsPatternsStack } from ‘../lib/sls-patterns-stack.ts’; const app = new cdk.App(); const envKey = app.node.tryGetContext("environment") ?? "dev"; // default: dev let parameter; if (envKey === "dev") { parameter = devParameter; } else { parameter = prodParameter; } new SlsPatternsStack(app, `SlsPatternsStack-${parameter.envName}`, { description: `${parameter.projectName}-${parameter.envName}`, env: { account: parameter.env?.account || process.env.CDK_DEFAULT_ACCOUNT, region: parameter.env?.region || process.env.CDK_DEFAULT_REGION, }, projectName: parameter.projectName, envName: parameter.envName, }); l cdk.jsonに定義するか、 パラメータ定義ファイルを作成して、 読み込ませる ./env_parameters.ts ./bin/sls-patterns.ts
  28. CDKでのファイル構成 69 ├── README.md ├── bin │ └── serverless-pattern-on-cdk.ts ├──

    cdk.json ├── cfn │ └── infrastructure.yaml ├── env ├── jest.config.js ├── lib │ └── event-driven-collaboration-stack.ts ## Stack: EventDrivenCollaborationStack の定義 ├── node_modules ├── package-lock.json ├── package.json ├── requirements.txt ├── resources │ └── event-driven-collaboration-stack ## Stack: EventDrivenCollaborationStack のLambda関数リソース │ └── lambda │ ├── t-_change_format.py │ └── t-_change_type.py ├── test └── tsconfig.json l Cfnテンプレートも CDKプロジェクト配下に 配置しておいて、 リポジトリ内で管理 l CDKのデプロイとは別に AWS CLI等で適⽤
  29. どれをインフラに含めるか問題 71 l VPC等ネットワーク系: インフラスタック l RDS等データベース系: インフラスタック l S3等ストレージ系:

    インフラスタック? l S3バケットをアプリスタックで: 複数⼈で開発やテストの際でも、簡単にデプロイできて確認 しやすい l S3バケットをインフラスタック: 複数⼈で開発やテストの際に、オブジェクトパスが被ってし まうケースもあるので、やりにくい 現状は、プロジェクト毎に判断してインフラとアプリを分けている
  30. JAWS PANKRATION 2024 74 l 8/24 (⼟) 20:40 ‒ 21:00

    で登壇予定 l TT-28「Serverless FrameworkからAWS CDKとAWS SAMに移⾏する際に⼼がけてい ること」をお話しします