A collection of notes and resources
自動オーケストレーションツールを使用して、継続的インテグレーションおよび継続的デリバリー (CI/CD) パイプラインを設定する
# buildspec.yml (CodeBuild)
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REPO
build:
commands:
- echo Build started on `date`
- docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
- docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $ECR_REPO/$IMAGE_REPO_NAME:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- docker push $ECR_REPO/$IMAGE_REPO_NAME:$IMAGE_TAG
- echo Writing image definitions file...
- printf '[{"name":"ml-container","imageUri":"%s"}]' $ECR_REPO/$IMAGE_REPO_NAME:$IMAGE_TAG > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
- appspec.yaml
Resources:
MLPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: ml-model-pipeline
RoleArn: !GetAtt CodePipelineRole.Arn
Stages:
# ソースステージ
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
RepositoryName: ml-model-repo
BranchName: main
OutputArtifacts:
- Name: SourceOutput
# ビルドステージ
- Name: Build
Actions:
- Name: BuildAction
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !Ref CodeBuildProject
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: BuildOutput
# デプロイステージ
- Name: Deploy
Actions:
- Name: DeployAction
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CREATE_UPDATE
StackName: ml-endpoint-stack
TemplatePath: BuildOutput::template.yaml
InputArtifacts:
- Name: BuildOutput
# CodeBuild ログの確認
aws codebuild batch-get-builds --ids $BUILD_ID
# CodePipeline 実行履歴
aws codepipeline get-pipeline-execution \
--pipeline-name ml-model-pipeline \
--pipeline-execution-id $EXECUTION_ID
# CodeDeploy デプロイ状況
aws deploy get-deployment --deployment-id $DEPLOYMENT_ID
main (本番)
├─ develop (開発)
│ ├─ feature/new-model (機能開発)
│ └─ feature/data-pipeline (機能開発)
└─ hotfix/bug-fix (緊急修正)
main
├─ feature/model-v2
└─ feature/new-feature
# GitHub Actions との統合例
name: ML Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
trigger-pipeline:
runs-on: ubuntu-latest
steps:
- name: Trigger CodePipeline
run: |
aws codepipeline start-pipeline-execution \
--name ml-model-pipeline
from sagemaker.workflow.pipeline import Pipeline
from sagemaker.workflow.steps import ProcessingStep, TrainingStep, CreateModelStep
from sagemaker.workflow.conditions import ConditionGreaterThanOrEqualTo
from sagemaker.workflow.condition_step import ConditionStep
from sagemaker.workflow.functions import JsonGet
# データ処理ステップ
processing_step = ProcessingStep(
name="PreprocessData",
processor=sklearn_processor,
inputs=[
ProcessingInput(
source=input_data,
destination="/opt/ml/processing/input"
)
],
outputs=[
ProcessingOutput(
output_name="train",
source="/opt/ml/processing/train"
),
ProcessingOutput(
output_name="validation",
source="/opt/ml/processing/validation"
)
],
code="preprocessing.py"
)
# トレーニングステップ
training_step = TrainingStep(
name="TrainModel",
estimator=xgb_estimator,
inputs={
"train": TrainingInput(
s3_data=processing_step.properties.ProcessingOutputConfig.Outputs["train"].S3Output.S3Uri
),
"validation": TrainingInput(
s3_data=processing_step.properties.ProcessingOutputConfig.Outputs["validation"].S3Output.S3Uri
)
}
)
# モデル評価ステップ
evaluation_step = ProcessingStep(
name="EvaluateModel",
processor=sklearn_processor,
inputs=[
ProcessingInput(
source=training_step.properties.ModelArtifacts.S3ModelArtifacts,
destination="/opt/ml/processing/model"
)
],
outputs=[
ProcessingOutput(
output_name="evaluation",
source="/opt/ml/processing/evaluation"
)
],
code="evaluation.py"
)
# 条件付きデプロイ
condition_step = ConditionStep(
name="CheckAccuracy",
conditions=[
ConditionGreaterThanOrEqualTo(
left=JsonGet(
step_name=evaluation_step.name,
property_file="evaluation",
json_path="metrics.accuracy"
),
right=0.8
)
],
if_steps=[create_model_step, deploy_step],
else_steps=[]
)
# パイプラインの作成
pipeline = Pipeline(
name="ml-training-pipeline",
steps=[
processing_step,
training_step,
evaluation_step,
condition_step
]
)
pipeline.upsert(role_arn=role)
pipeline.start()
{
"Comment": "ML Training Workflow",
"StartAt": "PrepareData",
"States": {
"PrepareData": {
"Type": "Task",
"Resource": "arn:aws:states:::sagemaker:createProcessingJob.sync",
"Parameters": {
"ProcessingJobName.$": "$.processingJobName",
"ProcessingInputs": [...],
"ProcessingOutputConfig": {...}
},
"Next": "TrainModel"
},
"TrainModel": {
"Type": "Task",
"Resource": "arn:aws:states:::sagemaker:createTrainingJob.sync",
"Parameters": {
"TrainingJobName.$": "$.trainingJobName",
"AlgorithmSpecification": {...},
"InputDataConfig": [...]
},
"Next": "EvaluateModel"
},
"EvaluateModel": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:EvaluateModel",
"Next": "CheckAccuracy"
},
"CheckAccuracy": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.accuracy",
"NumericGreaterThanEquals": 0.8,
"Next": "DeployModel"
}
],
"Default": "TrainingFailed"
},
"DeployModel": {
"Type": "Task",
"Resource": "arn:aws:states:::sagemaker:createEndpoint",
"End": true
},
"TrainingFailed": {
"Type": "Fail",
"Error": "AccuracyTooLow",
"Cause": "Model accuracy below threshold"
}
}
}
import boto3
events = boto3.client('events')
# スケジュールルールの作成(毎日午前2時)
events.put_rule(
Name='daily-model-training',
ScheduleExpression='cron(0 2 * * ? *)',
State='ENABLED'
)
# ターゲットの設定(SageMaker トレーニングジョブ)
events.put_targets(
Rule='daily-model-training',
Targets=[
{
'Id': '1',
'Arn': 'arn:aws:sagemaker:us-east-1:123456789012:pipeline/ml-training-pipeline',
'RoleArn': role_arn,
'SageMakerPipelineParameters': {
'PipelineParameterList': [
{
'Name': 'InputData',
'Value': f's3://{bucket}/data/latest/'
}
]
}
}
]
)
import boto3
import json
sagemaker = boto3.client('sagemaker')
def lambda_handler(event, context):
# S3イベントからトリガー
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
# パイプライン起動
response = sagemaker.start_pipeline_execution(
PipelineName='ml-training-pipeline',
PipelineParameters=[
{
'Name': 'InputData',
'Value': f's3://{bucket}/{key}'
}
]
)
return {
'statusCode': 200,
'body': json.dumps(f'Pipeline started: {response["PipelineExecutionArn"]}')
}
# test_preprocessing.py
import unittest
from preprocessing import clean_data, encode_features
class TestPreprocessing(unittest.TestCase):
def test_clean_data(self):
raw_data = {'col1': [1, 2, None, 4]}
cleaned = clean_data(raw_data)
self.assertIsNotNone(cleaned['col1'][2])
def test_encode_features(self):
data = {'category': ['A', 'B', 'A']}
encoded = encode_features(data)
self.assertEqual(len(encoded.columns), 2)
if __name__ == '__main__':
unittest.main()
# test_integration.py
def test_model_endpoint():
import boto3
runtime = boto3.client('sagemaker-runtime')
# テストデータで推論
response = runtime.invoke_endpoint(
EndpointName='test-endpoint',
ContentType='application/json',
Body='{"features": [1, 2, 3, 4]}'
)
result = json.loads(response['Body'].read())
assert 'predictions' in result
assert len(result['predictions']) > 0
# test_e2e.py
def test_full_pipeline():
import sagemaker
# パイプライン実行
pipeline = sagemaker.workflow.pipeline.Pipeline(name='ml-pipeline')
execution = pipeline.start()
# 完了を待機
execution.wait()
# 結果を検証
assert execution.describe()['PipelineExecutionStatus'] == 'Succeeded'
# モデルメトリクスを確認
steps = execution.list_steps()
eval_step = [s for s in steps if s['StepName'] == 'EvaluateModel'][0]
# メトリクスが閾値を超えているか確認
# ...
version: 0.2
phases:
install:
commands:
- pip install -r requirements.txt
- pip install pytest pytest-cov
pre_build:
commands:
- echo Running unit tests...
- pytest tests/unit/ -v --cov=src
build:
commands:
- echo Running integration tests...
- pytest tests/integration/ -v
post_build:
commands:
- echo Build completed
- echo Running e2e tests...
- pytest tests/e2e/ -v
reports:
test-results:
files:
- 'test-results.xml'
file-format: 'JUNITXML'
coverage-report:
files:
- 'coverage.xml'
file-format: 'COBERTURAXML'
import boto3
from datetime import datetime
cloudwatch = boto3.client('cloudwatch')
sagemaker = boto3.client('sagemaker')
def check_model_drift():
# CloudWatch からモデル精度メトリクスを取得
response = cloudwatch.get_metric_statistics(
Namespace='AWS/SageMaker',
MetricName='ModelAccuracy',
Dimensions=[
{'Name': 'EndpointName', 'Value': 'production-endpoint'}
],
StartTime=datetime.now() - timedelta(days=7),
EndTime=datetime.now(),
Period=3600,
Statistics=['Average']
)
avg_accuracy = sum([d['Average'] for d in response['Datapoints']]) / len(response['Datapoints'])
# 精度が閾値を下回ったら再トレーニング
if avg_accuracy < 0.75:
trigger_retraining()
def trigger_retraining():
# パイプライン起動
response = sagemaker.start_pipeline_execution(
PipelineName='ml-retraining-pipeline',
PipelineParameters=[
{
'Name': 'ReasonForRetraining',
'Value': 'ModelDrift'
}
]
)
print(f"Retraining triggered: {response['PipelineExecutionArn']}")
{
"source": ["aws.cloudwatch"],
"detail-type": ["CloudWatch Alarm State Change"],
"detail": {
"alarmName": ["ModelAccuracyAlarm"],
"state": {
"value": ["ALARM"]
}
}
}
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ Source │────>│ Build │────>│ Test │
│ (CodeCommit)│ │ (CodeBuild) │ │ (Pytest) │
└─────────────┘ └──────────────┘ └────────────┘
│
v
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ Deploy │<────│ Approve │<────│ Register │
│ (SageMaker) │ │ (Manual) │ │ (Registry)│
└─────────────┘ └──────────────┘ └────────────┘