AWS活用における便利な小技を簡潔に紹介する連載「AWSチートシート」。今回は、「AWS CloudFormation」内でコマンドが用意されていないインフラを「カスタムリソース」を使って自動構築させる方法を紹介する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
「Amazon Web Services」(AWS)活用における便利な小技を簡潔に紹介する連載「AWSチートシート」。今回は「AWS CloudFormation」内でコマンドが用意されていないインフラを自動構築したい場合に、CloudFormationの「カスタムリソース」を使って自動構築させる方法を紹介します。
公式ドキュメントでは、CloudFormationを下記のように定義しています。
AWS CloudFormationはAmazon Web Servicesリソースのモデル化およびセットアップに役立つサービスです。リソース管理に割く時間を減らし、AWSで実行するアプリケーションにさらに注力できるようになります。使用するすべてのAWSリソース(Amazon EC2インスタンスやAmazon RDS DBインスタンスなど)を記述するテンプレートを作成すれば、AWS CloudFormationがお客様に代わってこれらのリソースのプロビジョニングや設定を受け持ちます。AWSリソースを個別に作成、設計して、それぞれの依存関係を考える必要はありません。AWS CloudFormationがすべてを処理します。
つまりCloudFormationは、「用意したテンプレートに基づいてインフラ環境を自動構築できるサービス」です。下記のような要望が上がったら、CloudFormationの出番です。
CloudFormationには下記のような特徴があります。
なおCloudFormationの利用自体は無料なので気兼ねなく使うことができます(もちろんCloudFormationで立てたEC2やRDSなどに対しては料金が発生します)。
CloudFormation内で出てくる「テンプレート」「スタック」について簡単に説明します。
どういうリソースが欲しいかについて、JSONかYAMLのどちらかで記載したファイルのことです。依存関係や順番もまとめて記載できます。
例えば、新しく「AWS Identity and Access Management」(IAM)ロールを作り、そのロールを使って「AWS Lambda」内で別の処理をさせたい場合、IAMが完成するまでLambdaの処理を待たせることができます。
テンプレートから作られたリソースの集合体のことです。スタック単位で管理できるので、ワンクリックで、ひも付いた全てのリソースを削除するといったことができます。
テンプレートとスタックは下図のような関係にあります。
テンプレートをJSON/YAML形式で書いて、それをCloudFormationに入れることでリソースを作ります。その際にできたリソース全体をひとまとめに「スタック」といいます。
ほとんどの場合、このCloudFormationやそれに派生する「AWS Serverless Application Model」(SAM)、「Amazon Elastic Container Service」(ECS)、「AWS Amplify」といった、より便利なサービスが登場していることもあり、インフラの自動構築はそれらで事足りることが多いでしょう。ですが、AWS側の都合なのか、SDK(Pythonなら「Boto3」)では用意されているのに、CloudFormationでは用意されていないコマンドがあります。
そういった場合は「通常のCloudFormationの書き方では実行できないので諦めるかしかないか……」というとそうでもなく、これから紹介するカスタムリソースを使えばCloudFormationの中で実行可能です。
まずは公式ドキュメントから定義を確認します。
カスタムリソースを使用すると、テンプレートにカスタムのプロビジョニングロジックを記述し、ユーザーがスタックを作成、更新(カスタムリソースを変更した場合)、削除するたびにAWS CloudFormationがそれを実行します。たとえば、AWS CloudFormationのリソースタイプとして使用できないリソースを含める必要があるとします。それらのリソースは、カスタムリソースを使用して含めることができます。この方法により、すべての関連リソースを1つのスタックで管理できます。
もっと砕いた言い方をすると、カスタムリソースとは、下記のような方法ということです。
※基本的にはサポートされていない処理を積極的に組み込むので推奨というわけではありません。ですが、どうしても処理上1つのCloudFormationで済ませたい場合は、これを使うしかありません。押さえておくと、いざというときに便利です。
カスタムリソースを使ってリソースを作成します。先日業務内で、あらかじめ「Amazon Sagemaker Studio」で独自のConfigを適用したドメインを、CloudFormationで作りたい」という課題がありました。
CloudFormationのドキュメント「AWS::SageMaker::Domain」を調べると、ドメインを作ることはできそうでしたが、Configの作成はCloudFormationでサポートされていないようです。
ただ、CloudFormationにはなかったものの、Boto3には用意されていました(create_studio_lifecycle_config)。
今回はどうしてもこの1つのテンプレート内で処理したかったので、カスタムリソースを使うことにしました。
今回書いたコードはこちらです。
AWSTemplateFormatVersion: "2010-09-09" Description: "サンプルテンプレートYAMLファイル" Resources: GetSageMakerStudioDomainId: Type: Custom::CreateSageMakerStudioDomainLambda Version: 1.0 Properties: ServiceToken: !GetAtt CreateSageMakerStudioDomainLambda.Arn ExecutionRoleArn: !GetAtt AmazonSageMakerCustomExecutionRole.Arn CreateSageMakerStudioDomainLambda: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import json import boto3 import cfnresponse def lambda_handler(event, context): print(event) try: #まずStudioLifecycleConfigを作成する。 str=''' #SagemakerStudioが立ち上がったときに適用されてほしいコマンドを入力 mkdir -p .sampleFolder''' #========================== #studio_lifecycle_configの作成 smclient = boto3.client('sagemaker') config = smclient.create_studio_lifecycle_config( StudioLifecycleConfigName=config1', StudioLifecycleConfigContent=Content, StudioLifecycleConfigAppType='KernelGateway', ) configArn = config["StudioLifecycleConfigArn"] print(configArn) #========================== #ここからはDomainの作成に必要な「Amazon Virtual Private Cloud 」(VPC)とSubnetを取得 ec2client = boto3.client('ec2') vpclist = ec2client.describe_vpcs() default_vpcid = vpclist['Vpcs'][0]['VpcId'] subnets = ec2client.describe_subnets(Filters=[{'Name':'vpc-id', 'Values':[default_vpcid]}]) subnetid = subnets['Subnets'][0]['SubnetId'] #========================== #Domainを作成(カスタムリソース外でも構わないが、今回は一緒にLambda内で実行) domain = smclient.create_domain( DomainName='SandboxSageMakerStudioDomain', AuthMode='IAM', DefaultUserSettings={ 'ExecutionRole':event['ResourceProperties']['ExecutionRoleArn'], 'JupyterServerAppSettings': { 'DefaultResourceSpec': { 'InstanceType':'system', 'LifecycleConfigArn': configArn }, 'LifecycleConfigArns': [ configArn, ] }, 'KernelGatewayAppSettings': { 'LifecycleConfigArns': [ configArn, ] } }, SubnetIds=[subnetid,], VpcId=default_vpcid, ) print(domain) response = domain cfnresponse.send(event, context, cfnresponse.SUCCESS, response) print(response) except Exception as e: cfnresponse.send(event, context, cfnresponse.FAILED, {}) print(e) Handler: index.lambda_handler MemorySize: 128 Role: !GetAtt AmazonSageMakerCustomExecutionRole.Arn Runtime: python3.6 Timeout: 60 AmazonSageMakerCustomExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: [lambda.amazonaws.com, sagemaker.amazonaws.com] Action: sts:AssumeRole Path: / Policies: - PolicyName: createSageMakerDomain PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: "*" Action: [ "sagemaker:CreateDomain**", "sagemaker:DeleteDomain**", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "sts:AssumeRole", ] ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess" - "arn:aws:iam::aws:policy/AmazonVPCFullAccess" # Outputs:
カスタムリソースに関して重要なポイントを解説します。
上記で指定することで同じテンプレート内の「Lambda名」のLambdaを実行しています。加えてPropertiesのServiceTokenでLambdaのArnを指定する必要があるのでご注意ください。
GetSageMakerStudioDomainId: Type: Custom::CreateSageMakerStudioDomainLambda Version: 1.0 Properties: ServiceToken: !GetAtt CreateSageMakerStudioDomainLambda.Arn ExecutionRoleArn: !GetAtt AmazonSageMakerCustomExecutionRole.Arn
Properties内に記載した情報は、ResourcePropertiesとしてLambdaに渡されるのでLambda内の処理に使えます。
今回はExecutionRoleArnを渡しているので、Lambda内でSagemakerStudioのCreateコマンドを使う際に必要な権限を持ったIAMのArnを使えています。
※「!GetAtt」を使えば、「Lambdaが作成され、そのArnが吐き出されたらGetSageMakerStudioDomainIdにそのArnを渡す」という処理を書けます。
Lambda内での取り出し方は以下のイメージです。
event['ResourceProperties']['ExecutionRoleArn']
create_studio_lifecycle_configはCloudFormationのドキュメントではサポートされていませんが、SDKならサポートされているので、こちらをLambdaから実行しています。
smclient = boto3.client('sagemaker') config = smclient.create_studio_lifecycle_config( StudioLifecycleConfigName=custom_config1', StudioLifecycleConfigContent=Content, StudioLifecycleConfigAppType='KernelGateway' ) configArn = config["StudioLifecycleConfigArn"] print(configArn)
CloudFormationで先ほどのYAMLファイルを実行すると、リソースが無事作成されました。
!GetAttを使って作成される順番を指定していたので、順番は下記のようになっています。
IAMロール(AmazonSageMakerCustomExecutionRole)
↓
Lambda(CreateSageMakerStudioDomainLambda)
↓
カスタムリソース(GetSageMakerStudioDomainId)
カスタムリソースは「そこまで出番があるような機能か」と言われると正直、微妙ですが、現状CloudFormationがサポートしていないリソースを1つのテンプレート内で他の処理と一緒に一発で作りたい場合はこれを使うしかないはずなので覚えておいて損はないと思います。「CloudFormationサポート外のインフラを自動構築したい」というニッチなお悩みを持つ方に少しでもお役に立てば幸いです。
株式会社システムシェアード xTechLab事業部所属。
普段はWeb技術、直近はAI、機械学習、IoT、サーバレスアプリケーションなど先端技術を用いて主体的に開発する。技術を探究するのはもちろん、産業×ITがどのように社会に利益を生み出せるかという観点の下、素晴らしい技術がもっと世間に広まるよう活動している。保有AWS認定資格は、「AWS認定ソリューションアーキテクト」「Alexa Skill Builder」など。
Copyright © ITmedia, Inc. All Rights Reserved.