Github Actions と CodePipeline を使って Step Functions を動かす CI/CD を構築
Table of Contents
2ヶ月以上ご無沙汰になってしまいましたが,久しぶりのテックブログです.今回はタイトルにもあるように,Github Actions と CodePipeline を使ってマージトリガーで Step Functions のパイプラインを動かす CI/CD を構築したお話になります.
今回のモチベーションは,3つほどあります.
- ML 系のモデル学習パイプラインの構築と DryRun 的なものを毎回手動で実行するのがいいかげんめんどくさくなってきた
- 人数が少ない ML チームだと担当者が対応できない場合に,属人化したものを代わりにオペレーションするのが大変でオペミスが発生する可能性がある
- この辺りはドキュメント整備やチーム内での共有といった部分を整理しておく必要があるのは理解しつつ...
- CI/CD 周りの設定含めてもう少し知識を付けて MLOps のレベルを上げたかった
ML 系プロジェクトにおいて,CI/CD 整備の優先度が低かったり,そもそもソフトウェアエンジニアに比べてこの辺りの経験や知識が豊富でないということで後回しにされがちですが,MLOps を考える上で CI/CD は大事なファクターの1つなのでしっかり取り組むべきです(CI/CD の自動化は Google が定義している MLOps level 2: CI/CD pipeline automation に相当する部分).また,少人数チームの場合は尚のこと,人手をかけられない + 属人化を排除する意味でも取り入れたいです.
以下のリポジトリにソースコードを置いておきます.
CI/CD パイプラインの構成
※ はじめに,CodePipeline の設定や IAM ロールの必要な権限は詳細に説明しないのでご了承くださいませ.公式ドキュメントや巷にある詳細な説明がされているブログをご覧下さい.
今回のゴールは Github のブランチマージから最終的に Step Functions のパイプラインを動かすところまでになります.本来は Step Functions 内で ML のモデル学習を行うパイプラインを構築しますが,今回はサンプルとして SageMaker ProcessingJob を単発で動かすだけです.
今回構築した CI/CD パイプラインは以下のような感じで,
ディレクトリ構成は以下になります.
.
├── .github
│ └── workflows
│ └── sam-codepipeline.yaml
├── .gitignore
├── README.md
├── async-processing
├── cicd-pipeline
│ ├── README.md
│ ├── config
│ │ ├── buildspec.yml
│ │ └── dev-codepipeline-ver1.json
│ ├── container
│ │ ├── Dockerfile
│ │ ├── app
│ │ │ └── src
│ │ │ ├── hello.py
│ │ │ └── logger.py
│ │ ├── docker-compose.yml
│ │ ├── requirements.lock
│ │ └── requirements.txt
│ └── sam
│ ├── env
│ │ ├── dev
│ │ │ ├── samconfig.toml
│ │ │ └── template.yaml
│ │ └── prod
│ │ ├── samconfig.toml
│ │ └── template.yaml
│ └── statemachine
│ └── sample-ml-pipelines-ver1.asl.json
└── teamaya
流れを説明していくと,
-
Github Actions パート
sam-codepipeline.yaml
name: sam-stepfunctions-codepipeline on: pull_request: branches: - dev - main types: [opened] # paths: # - 'cicd-pipeline/config/dev-codepipeline-ver1.json' # - './github/workflows/sam-codepipeline.yaml' workflow_dispatch: jobs: Build-Deploy-SAM: name: Build & Deploy SAM for Pipeline runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout uses: actions/checkout@v2 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 # samによるdev/prod環境のAWSリソース更新 - name: Build SAM & Deploy SAM run: | if ${{ github.base_ref == 'dev' }}; then cd sam/env/dev sam build sam deploy --fail-on-empty-changeset --no-confirm-changeset elif ${{ github.base_ref == 'main' }}; then cd sam/env/prod sam build sam deploy --fail-on-empty-changeset --no-confirm-changeset else echo "Invalid branch name." exit 1 fi Update-CodePipeline: name: Update Codepipeline for Step Functions runs-on: ubuntu-latest timeout-minutes: 5 needs: Build-Deploy-SAM steps: - name: Checkout uses: actions/checkout@v2 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: Update Codepipeline run: aws codepipeline update-pipeline --pipeline file://config/codepipeline-ver1.json
- Pull Reqest が Open したタイミングで Github Actions が走る
- AWS Serverless Application Model (SAM) の Build と Deploy を行う
- マージ先のブランチに応じて,切り替わるようにしています.dev マージでは dev 用のリソースを作成し,main マージでは prod 用のリソースを作成する.
- AWS CLI を使って CodePipeline の action configuration を更新する
- Step Functions の StateMachineArn が ML のモデル学習のバージョンによって変更されることがあるので,後続の CodePipeline で動かす対象の Step Functions を更新する.
-
CodePipeline パート
buildspec.yml
version: 0.2 env: variables: ENV: "dev" REPOSITORY_NAME: <ECRのリポジトリ名> IMAGE_TAG: "latest" REGION: "ap-northeast-1" parameter-store: AWS_ACCOUNT_ID: "/CodeBuild/common/AWS_ACCOUNT_ID" AWS_ACCESS_KEY_ID: "/CodeBuild/common/AWS_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY: "/CodeBuild/common/AWS_SECRET_ACCESS_KEY" phases: pre_build: commands: # - echo Login to Docker # - docker login --username $AWS_ACCESS_KEY_ID --password $AWS_SECRET_ACCESS_KEY - echo Set ECR repository URI - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $REPOSITORY_URI build: commands: - echo Build started - echo Building the Docker Image - docker build -t $REPOSITORY_URI/$REPOSITORY_NAME:$IMAGE_TAG container post_build: commands: - echo Login to Amazon ECR - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $REPOSITORY_URI - echo Pushing the Docker Image to ECR started - docker push $REPOSITORY_URI/$REPOSITORY_NAME:$IMAGE_TAG
- PR が dev/main にマージされたタイミングで AWS で事前に設定している CodePipeline が走る
- 事前に CodePipeline 上で組んでいるフローが動く
- CodeBuild が起動し,Docker Image の Build が行われ,Image を ECR に push する
- Step Functions が起動し,パイプラインが走る
- ECR に登録している Image を使って,SageMaker ProcessingJob が動く
細かい部分で言うと,
AWS_ACCOUNT_ID
,AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
などの機密情報は,パラメータストア(AWS Systems Manager Parameter Store)へ登録しておき,それを参照する形で使うようにしています. - PR が dev/main にマージされたタイミングで AWS で事前に設定している CodePipeline が走る
AWS Serverless Application Model
AWS Serverless Application Model (SAM) とは AWS でサーバーレスアプリケーションを簡単に構築することがフレームワークです.CloudFormation の拡張で CloudFormation で利用できるリソースは SAM でも使用することができます.YAML もしくは JSON 形式のテンプレートを使って簡単に環境構築ができるし,CLI も提供されています.詳しくは公式の「AWS Serverless Application Model (AWS SAM) とは」を見ると良いです.
設定するファイルは2つあります.
- samconfig.toml
- template.yaml
dev 環境の設定ファイルを見ていくと,
-
samconfig.toml
samconfig.toml
version = 0.1 [default] [default.deploy.parameters] stack_name = "dev-sample-codepipeline" s3_bucket = "aws-sam-cli-managed-samclisourcebucket-dev-sample-codepipeline" s3_prefix = "dev-sample-codepipeline" region = "ap-northeast-1" confirm_changeset = true capabilities = "CAPABILITY_IAM" disable_rollback = true
- こちらのファイルは1つのファイルに dev と prod の両方の設定を実装することもできるが,今回は環境毎にファイルを分けた(後述するテンプレートと一緒に管理する必要があるが,微妙に環境毎で変数が違う部分もあるのでこのファイルも分けて管理した方が良いかなと思い分けている).
[default.deploy.parameters]
はsam deploy
コマンドが実行された時に渡される引数になる.- 注意としては,
s3_bucket
は事前に作成しておかないとデプロイした際にS3 Bucket does not exist.
といったエラーが発生する.- S3 には,ビルドしたテンプレートファイルとリソースファイルが保存される.
-
template.yaml
template.yaml
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Create Resource - StepFunctions - EventBridge Parameters: EnvironmentVariable: Description: 環境変数 Type: String Default: dev VersionVariable: Description: バージョン番号 Type: String Default: ver1 StepFunctionsExecutionRole: Description: Step Functionsの実行ロール Type: String Default: arn:aws:iam::<AWSアカウントID>:role/StepFunctionsExecutionRole SageMakerProcessingImage: Description: SageMakerのProcessingJobを動かすImage Type: String Default: <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<ECRのリポジトリ名>:latest Resources: # =======Step Functions for ProcessingJob======== # DevMLPipelinesStateMachine: Type: AWS::Serverless::StateMachine Properties: Name: !Sub ${EnvironmentVariable}-sample-ml-pipelines-${VersionVariable} DefinitionUri: ../../statemachine/sample-ml-pipelines-ver1.asl.json DefinitionSubstitutions: ProcessingJobRole: !Ref StepFunctionsExecutionRole ProcessingImage: !Ref SageMakerProcessingImage ProcessingEnvironment: !Ref EnvironmentVariable Role: !Ref StepFunctionsExecutionRole Events: Schedule: Type: Schedule Properties: Description: パイプライン用のスケジューラー Enabled: False Name: !Sub ${EnvironmentVariable}-sample-ml-pipelines-${VersionVariable} Schedule: "cron(0 16 * * ? *)"
- JSON ではなく,YAML 形式で書けるので良い.
- Parameters ブロックでは,値を変数化できるので,共通設定や dev/prod で動的に変わる部分だったりを書いておくと使い回しやすい.
- Resources ブロックでは,Parameters ブロックで定義した変数を
!Ref
や!Sub
で使うことができる.!Sub
は値の一部に変数を使用したい時に使うことができる.
- Role の設定もできるが,今回は事前に設定しておいた IAM ロールを使用している.
- 今回は Step Functions だけを定義したので,定義ファイル
sample-ml-pipelines-ver1.asl.json
を次に見ていく.
-
sample-ml-pipelines-ver1.asl.json
sample-ml-pipelines-ver1.asl.json
{ "Comment": "Sample ML pipelines", "StartAt": "SageMaker-Hello-World", "States": { "SageMaker-Hello-World": { "Comment": "Hello Worldを出力する", "Type": "Task", "Resource": "arn:aws:states:::sagemaker:createProcessingJob.sync", "Parameters": { "RoleArn": "${ProcessingJobRole}", "ProcessingJobName.$": "States.Format('{}', $$.Execution.Name)", "AppSpecification": { "ImageUri": "${ProcessingImage}", "ContainerEntrypoint": [ "python3", "/opt/program/src/hello.py" ] }, "ProcessingResources": { "ClusterConfig": { "InstanceCount": 1, "InstanceType": "ml.t3.medium", "VolumeSizeInGB": 10 } }, "Environment": { "PYTHON_ENV": "${ProcessingEnvironment}" }, "StoppingCondition": { "MaxRuntimeInSeconds": 86400 } }, "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "FailState" } ], "End": true }, "FailState": { "Type": "Fail", "Cause": "Error", "Error": "Error" } } }
- JSON 形式で書かれた Step Functions の定義ファイルになる.
- Hello World を出力するだけの内容になっているが,それを SageMaker ProcessingJob で動かしている.今回は単純な処理だが,ML モデルを構築するためのパイプラインをここに実装すれば,その内容が SAM で構築される.
- パイプラインを構築する際は,Step Functions の Workflow studio で直感的な GUI で簡単に作成できるので,それで作成した後に JSON の定義ファイルをDLすれば同じものを本番環境用にサクッと構築することができる.
- ProcessingJob を動かす Image は CodeBuild 時に ECR に push したものを使っている.
- また,
ProcessingJobName
は一意でないとエラーになるので,Context オブジェクトの$$.Execution.Name
を使用している.
AWS CodePipeline
CodePipeline は複数のステージというものが用意されていて,それを繋ぎ合わせて一連のパイプラインを構築し CI/CD を自動化します.詳しくは公式の「AWS CodePipeline とは」を見ると良いです.
今回の場合,Source → Build → Execute (Invoke) のような流れになっています.
- Source: Github のブランチマージトリガーで起動する
- Build: Docker ImageのBuild と ECR への push を行う
- Execute (Invoke): Step Functions を起動する
- 処理が正常に終了すると,Step Functions の実行結果と CloudWatch Logs のログは以下のようになります.
- CodePipeline を動かす時の注意としては,権限周りのエラーがよく発生するので CodePipeline から何を動かす必要があるかをチェックして動かすアクションの権限を与えてやる必要があります.
- 今回の場合:
- CodePipeline では,CodeBuild と Step Functions を動かすためのポリシーが必要
- CodeBuild では,ECR と SystemsManager を操作するためのポリシーが必要
- Step Functions では,SageMaker の操作と ECR へのアクセスを行うポリシーが必要
- エラー周りは参考に挙げたブログが役に立ちます.
- 今回の場合:
今回は Github Actions と CodePipeline を使って Step Functions を動かす CI/CD パイプラインを構築したお話でした.
Step Functions の中身をMLモデル学習のパイプラインとすれば,Github Actions の PR が Open とブランチマージをトリガーとして,CodePipeline が走ることで,一連の流れを CI/CD で実現できることになります.
ここにテストを追加したり,例えば,ECS で ML-API が動いている場合には,Step Functions のパイプラインが正常終了した後に,承認プロセスを入れて ECS のタスク定義とサービスの更新を入れることで,デプロイまで持っていくことができるかなと!
もう少し発展させることでより良い開発体験が生み出すことができるのと,MLOps の成熟度も上がって運用の自動化も一歩前進すると考えています.