検索
連載

「AWS CloudFormation」内でコマンドが用意されていないインフラを「カスタムリソース」を使って自動構築させるAWSチートシート

AWS活用における便利な小技を簡潔に紹介する連載「AWSチートシート」。今回は、「AWS CloudFormation」内でコマンドが用意されていないインフラを「カスタムリソース」を使って自動構築させる方法を紹介する。

PC用表示 関連情報
Share
Tweet
LINE
Hatena

 「Amazon Web Services」(AWS)活用における便利な小技を簡潔に紹介する連載「AWSチートシート」。今回は「AWS CloudFormation」内でコマンドが用意されていないインフラを自動構築したい場合に、CloudFormationの「カスタムリソース」を使って自動構築させる方法を紹介します。

CloudFormationとは

 公式ドキュメントでは、CloudFormationを下記のように定義しています。

AWS CloudFormationはAmazon Web Servicesリソースのモデル化およびセットアップに役立つサービスです。リソース管理に割く時間を減らし、AWSで実行するアプリケーションにさらに注力できるようになります。使用するすべてのAWSリソース(Amazon EC2インスタンスやAmazon RDS DBインスタンスなど)を記述するテンプレートを作成すれば、AWS CloudFormationがお客様に代わってこれらのリソースのプロビジョニングや設定を受け持ちます。AWSリソースを個別に作成、設計して、それぞれの依存関係を考える必要はありません。AWS CloudFormationがすべてを処理します。

 つまりCloudFormationは、「用意したテンプレートに基づいてインフラ環境を自動構築できるサービス」です。下記のような要望が上がったら、CloudFormationの出番です。

  • AWSリソースの管理、構築を効率化したい
  • 開発標準に基づいてインフラを作成、更新したい
  • リソースの依存関係やプロビジョニングの順序を確実にしたい

 CloudFormationには下記のような特徴があります。

  • 一度テンプレートを作成すれば、同じ構成を再現できる
    • 開発環境の構築
    • ブログシステム、Webシステム、ゲームプラットフォームなど、同じ仕組みでアプリやデータが異なるようなもの
  • ベストプラクティスが盛り込まれたテンプレートが使用可能
    • 複数のAZ(Availability Zone)をまたいでリソースを配置する可用性の高い構成
    • セキュリティ要件を満たす上での必須ソフト、設定が入った構成
  • 起動時にパラメーターを渡せる
    • 例えばDBのエンドポイントをEC2に渡せる

 なお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つのスタックで管理できます。

 もっと砕いた言い方をすると、カスタムリソースとは、下記のような方法ということです。

  • CloudFormationがサポートしていないリソースを
  • それでもCloudFormation内でまとめて管理したいときに
  • Lambdaを使いテンプレート内でSDKを使うことで
  • 無理やりテンプレート内に収めてしまおう

※基本的にはサポートされていない処理を積極的に組み込むので推奨というわけではありません。ですが、どうしても処理上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:

 カスタムリソースに関して重要なポイントを解説します。

【1】Type: Custom::Lambda名

 上記で指定することで同じテンプレート内の「Lambda名」のLambdaを実行しています。加えてPropertiesのServiceTokenでLambdaのArnを指定する必要があるのでご注意ください。

 GetSageMakerStudioDomainId:
    Type: Custom::CreateSageMakerStudioDomainLambda
    Version: 1.0
    Properties:
        ServiceToken: !GetAtt CreateSageMakerStudioDomainLambda.Arn
        ExecutionRoleArn: !GetAtt AmazonSageMakerCustomExecutionRole.Arn

【2】Properties内にLambda内で使いたいパラメーターを渡すことができる

 Properties内に記載した情報は、ResourcePropertiesとしてLambdaに渡されるのでLambda内の処理に使えます。

 今回はExecutionRoleArnを渡しているので、Lambda内でSagemakerStudioのCreateコマンドを使う際に必要な権限を持ったIAMのArnを使えています。

※「!GetAtt」を使えば、「Lambdaが作成され、そのArnが吐き出されたらGetSageMakerStudioDomainIdにそのArnを渡す」という処理を書けます。

 Lambda内での取り出し方は以下のイメージです。

event['ResourceProperties']['ExecutionRoleArn']

【3】CloudFormationのサポート外コマンドをLambdaで実行

 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.

ページトップに戻る