発表されてからだいぶ経ちますが、複数サービスのリソース情報を横断的に取得する社内ツールで活用できないかと思いAWS Cloud Control APIについて簡単に調べてみました。
基本的な操作に関する実際のコードなどは下記AWS News Blogにあるので割愛しています。
AWS Cloud Control API, a Uniform API to Access AWS & Third-Party Services
サービス概要
- サードパーティ含め、さまざまなサービスのリソースのCRUD-L操作に対して統一されたAPIを提供するサービス。
- CRUD-L:Create(作成), Read(読み取り), Update(更新), Delete(削除), and List(一覧表示)
- 実行時は各操作にパラメーターとしてCloudFormationで見慣れたリソースタイプと属性(JSON)or 識別子を渡す。
- 戻り値の型やエラーメッセージも同様に、すべての操作とリソースで統一されている。
- リソースの管理をおこなうツールなど低レベルのAWSサービスAPIを必要とする場合、Cloud Control APIを利用すると各サービスごとに特化したコードを削減でき、コードの運用保守や新しいサービス・機能への追従が容易になる。
従来はサービスごとに異なるAPIを学習し、それぞれ固有のコードやスクリプトを作成する必要がありましたが、Cloud Control APIの統一されたAPIを利用することで共通化しシンプルにできるとのことです。
たとえば、Lambdaの関数リストとCloudFormationのスタックリストを取得する場合、それぞれListFunctions
、ListStacks
を呼び出す必要がありましたが、Cloud Control APIならばどちらも同じListResources
を呼び出すことで一覧を取得可能になります。
オペレーションの種類
Cloud Control APIには大きく分けて、リソース操作のリクエストを行うためのAPIオペレーションと、そのリソース操作リクエストを管理するためのAPIオペレーションの2種類があります。
リソース操作リクエストのAPIオペレーション
リソース操作のリクエストを行うAPIオペレーションは下記の5つです。
- CreateResource:リソースを作成する。
- UpdateResource:リソースを更新する。
- GetResource:リソースの詳細を取得する。
- DeleteResource:リソースを削除する。
- ListResources:リソースの一覧を取得する。
すべての操作はCloud Control APIで作成したリソース以外に対しても同様に実行可能です。つまり、他のIaCサービス(CloudFormationやTerraformなど)で管理しているリソースに対しても操作可能なので、UpdateResourceやDeleteResourceといった破壊的操作をする際は注意が必要です。
リソース操作リクエストを管理するAPIオペレーション
リソース操作で発行したリクエストを管理するためのAPIもあります。
- CancelResourceRequest:進行中(ステータスが
IN_PROGRESS
、PENDING
)のリクエストをキャンセルする。 - GetResourceRequestStatus:リクエストの進行状況を取得する。
- ListResourceRequests:アクティブなリクエストの一覧を取得する。
なお、リソース操作リクエストは7日後に期限切れになるとのことです。
セキュリティ・IAM関連
ドキュメントには
AWS CloudFormation provides the security architecture for Cloud Control API; because of this, you will need to configure CloudFormation to meet your security and compliance objectives when using Cloud Control API.
と記載があるため、セキュリティ関連はCloudFormationでのセキュリティに準拠しますが一部異なる点もあるので注意が必要です。
Cloud Control APIの実行権限
Cloud Control APIのアクションはCloudFormationの一部であるため、プレフィックスとしてcloudformation
がついています。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "cloudformation:ListResources", "cloudformation:DeleteResource", "cloudformation:CancelResourceRequest", "cloudformation:GetResource", "cloudformation:UpdateResource", "cloudformation:GetResourceRequestStatus", "cloudformation:ListResourceRequests", "cloudformation:CreateResource" ], "Resource": "*" } ] }
ただし、2023/09/06時点ではCloudFormationリソースレベルの権限とCloudFormation条件の使用はサポートされていないとのことです。
サービスロールの利用
リソース操作リクエストを行うときに、RoleArn
パラメーターを指定することでリソース操作を行うサービスロールを指定できます。
前述の通り、Cloud Control APIのアクションはCloudFormationの一部であるためCloudFormationでサービスロールを利用するときと同様に、ロールを引き受けることのできるサービスとしてAWS CloudFormation (cloudformation.amazonaws.com
) を指定する必要があります。
リソースタイプ
現在対応しているリソースタイプ
現在サポートされているAWSのリソースタイプは下記で一覧できます。
リリース当初と比較するとAWS::S3::Bucket
なども追加されて充実してきています。しかし、2023/09/06時点ではAWS::EC2::Instance
など比較的利用頻度の高いリソースタイプでもまだ対応していないものが見受けられます。
サードパーティのものを含めたパブリックなリソースタイプはマネージドコンソールのCloudFormationレジストリからも確認可能です。ただし、ここで表示されるリソースタイプすべてがCloud Control APIをサポートしているわけではないので注意が必要です。
リソースタイプがCloud Control APIをサポートしているかの確認
cloudformation describe-type
コマンドを使用してリソースタイプの詳細を取得し、ProvisioningType
の値を確認することでそのリソースタイプがサポートされているか否かを判断できます。ProvisioningType
の値がFULLY_MUTABLE
、もしくはIMMUTABLE
の場合はサポートされています。
試しにAWS::Logs::LogGroup
について確認してみると、FULLY_MUTABLE
なのでサポート対象であることがわかります。
$ aws cloudformation describe-type --type RESOURCE --type-name AWS::Logs::LogGroup --query "ProvisioningType" "FULLY_MUTABLE"
それに対してAWS::EC2::Instance
の場合、NON_PROVISIONABLE
であるためサポート対象外です。
$ aws cloudformation describe-type --type RESOURCE --type-name AWS::EC2::Instance --query "ProvisioningType" "NON_PROVISIONABLE"
リソースタイプごとの属性や操作に必要な権限の確認
リソースタイプスキーマを確認することで、リソースを操作するときに必要な属性に関する情報や、可能な操作とそれに必要な権限を確認できます。この確認もマネージドコンソール、cloudformation describe-type
コマンドのどちらでも可能です。
AWS::Lambda::Function
を例にcloudformation describe-type
コマンドを利用して確認してみます。
jqの部分はSchema
の値を抽出して整形するための処理です。
$ aws cloudformation describe-type --type RESOURCE --type-name AWS::Lambda::Function | jq -r .Schema | jq .
返ってきた値を確認すると、properties
セクションには属性やそのデータタイプ、必須であるかどうか、許容値や必須パターンなどの制約が定義されています。
"properties": { "Description": { "description": "A description of the function.", "type": "string", "maxLength": 256 }, "TracingConfig": { "description": "Set Mode to Active to sample and trace a subset of incoming requests with AWS X-Ray.", "$ref": "#/definitions/TracingConfig" }, "VpcConfig": { "description": "For network connectivity to AWS resources in a VPC, specify a list of security groups and subnets in the VPC.", "$ref": "#/definitions/VpcConfig" }, "RuntimeManagementConfig": { "description": "RuntimeManagementConfig", "$ref": "#/definitions/RuntimeManagementConfig" }, "ReservedConcurrentExecutions": { "description": "The number of simultaneous executions to reserve for the function.", "type": "integer", "minimum": 0 }, "SnapStart": { "description": "The SnapStart setting of your function", "$ref": "#/definitions/SnapStart" }, "FileSystemConfigs": { "maxItems": 1, "description": "Connection settings for an Amazon EFS file system. To connect a function to a file system, a mount target must be available in every Availability Zone that your function connects to. If your template contains an AWS::EFS::MountTarget resource, you must also specify a DependsOn attribute to ensure that the mount target is created or updated before the function.", "type": "array", "items": { "$ref": "#/definitions/FileSystemConfig" } }, "FunctionName": { "minLength": 1, "description": "The name of the Lambda function, up to 64 characters in length. If you don't specify a name, AWS CloudFormation generates one.", "type": "string" }, "Runtime": { "description": "The identifier of the function's runtime.", "type": "string" }, "KmsKeyArn": { "pattern": "^(arn:(aws[a-zA-Z-]*)?:[a-z0-9-.]+:.*)|()$", "description": "The ARN of the AWS Key Management Service (AWS KMS) key that's used to encrypt your function's environment variables. If it's not provided, AWS Lambda uses a default service key.", "type": "string" }, ...
handlers
セクションには、リソースタイプが対応している操作と、その操作のために必要な権限が定義されています。
"handlers": { "read": { "permissions": [ "lambda:GetFunction", "lambda:GetFunctionCodeSigningConfig" ] }, "create": { "permissions": [ "lambda:CreateFunction", "lambda:GetFunction", "lambda:PutFunctionConcurrency", "iam:PassRole", "s3:GetObject", "s3:GetObjectVersion", "ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVpcs", "elasticfilesystem:DescribeMountTargets", "kms:CreateGrant", "kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "lambda:GetCodeSigningConfig", "lambda:GetFunctionCodeSigningConfig", "lambda:GetLayerVersion", "lambda:GetRuntimeManagementConfig", "lambda:PutRuntimeManagementConfig", "lambda:TagResource" ] }, "update": { "permissions": [ "lambda:DeleteFunctionConcurrency", "lambda:GetFunction", "lambda:PutFunctionConcurrency", "lambda:ListTags", "lambda:TagResource", "lambda:UntagResource", "lambda:UpdateFunctionConfiguration", "lambda:UpdateFunctionCode", "iam:PassRole", "s3:GetObject", "s3:GetObjectVersion", "ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVpcs", "kms:CreateGrant", "kms:Decrypt", "kms:GenerateDataKey", "lambda:GetRuntimeManagementConfig", "lambda:PutRuntimeManagementConfig", "lambda:PutFunctionCodeSigningConfig", "lambda:DeleteFunctionCodeSigningConfig", "lambda:GetCodeSigningConfig", "lambda:GetFunctionCodeSigningConfig" ] }, "list": { "permissions": [ "lambda:ListFunctions" ] }, "delete": { "permissions": [ "lambda:DeleteFunction", "ec2:DescribeNetworkInterfaces" ] } },
リソースタイプスキーマの詳細についてはリソースタイプ定義スキーマをご確認ください。
リソース操作リクエストの冪等性を保証する
リソースの作成CreateResource
、更新UpdateResource
、削除DeleteResource
のリクエストにはClientToken
パラメーターを指定できます。
これによってリクエストの冪等性が保証されます。
Identifier
であるFunctionName
を指定せずにLambda関数を作成する場合を例に確認してみます。
まず、client-token
を設定せずにcreate-resource
コマンドを実行します。
$ aws cloudcontrol create-resource \ --type-name AWS::Lambda::Function \ --desired-state '{"Code":{"S3Bucket":"cloudcontrol-test","S3Key":"sample_code.zip"},"Role":"arn:aws:iam::123456789012:role/service-role/cloudcontrol-test-role","Runtime":"python3.11","Handler":"lambda_function.lambda_handler"}' { "ProgressEvent": { "TypeName": "AWS::Lambda::Function", "RequestToken": "f7158354-42fc-4c81-819a-c61cb01607f4", "Operation": "CREATE", "OperationStatus": "IN_PROGRESS", "EventTime": "2023-09-07T00:50:27.215000+00:00" } }
次に、このリクエストのステータスを確認します。
$ aws cloudcontrol get-resource-request-status --request-token f7158354-42fc-4c81-819a-c61cb01607f4 { "ProgressEvent": { "TypeName": "AWS::Lambda::Function", "Identifier": "6PymYsTEpYWRnyIss4G2Nhx9j-YSi4mBv61sWK", "RequestToken": "f7158354-42fc-4c81-819a-c61cb01607f4", "Operation": "CREATE", "OperationStatus": "SUCCESS", "EventTime": "2023-09-07T00:50:33.361000+00:00" } }
OperationStatus
がSUCCESS
、Identifier
が6PymYsTEpYWRnyIss4G2Nhx9j-YSi4mBv61sWK
なので、関数名6PymYsTEpYWRnyIss4G2Nhx9j-YSi4mBv61sWK
というLambda関数が作成されたことがわかります。
続けて同じコマンドを再度実行して、リクエストのステータスを確認してみます。
意図せずに同じ設定のリクエストを多重送信してしまった想定です。
$ aws cloudcontrol create-resource \ --type-name AWS::Lambda::Function \ --desired-state '{"Code":{"S3Bucket":"cloudcontrol-test","S3Key":"sample_code.zip"},"Role":"arn:aws:iam::123456789012:role/service-role/cloudcontrol-test-role","Runtime":"python3.11","Handler":"lambda_function.lambda_handler"}' { "ProgressEvent": { "TypeName": "AWS::Lambda::Function", "RequestToken": "aec3963b-a73f-4492-b38d-806dc9620ac5", "Operation": "CREATE", "OperationStatus": "IN_PROGRESS", "EventTime": "2023-09-07T00:50:37.793000+00:00" } } $ aws cloudcontrol get-resource-request-status --request-token aec3963b-a73f-4492-b38d-806dc9620ac5 { "ProgressEvent": { "TypeName": "AWS::Lambda::Function", "Identifier": "5kOz9vkTaOJswXEHV8P6AJVVS-NoA3niuHCQxu", "RequestToken": "aec3963b-a73f-4492-b38d-806dc9620ac5", "Operation": "CREATE", "OperationStatus": "SUCCESS", "EventTime": "2023-09-07T00:50:43.878000+00:00" } }
さきほどとはRequestToken
が異なるため別のリクエストとして処理されていることがわかります。OperationStatus
、Identifier
を確認すると、まったく同じ設定で別のLambda関数5kOz9vkTaOJswXEHV8P6AJVVS-NoA3niuHCQxu
が作成されています。
この例ではIdentifier
を指定していないため複数のリソースが作成される結果となっていますが、たとえばIdentifier
やその他競合が発生してしまうような設定をしたうえで、作成処理に時間がかかるリソースのCreateResource
が多重でリクエストされてしまった場合、意図せぬエラーの発生や想定外のリソースが作成されてしまうことが予想されます。
次にClientToken
パラメーターを指定して、複数回おなじコマンドを連続実行してみます。
$ aws cloudcontrol create-resource \ --type-name AWS::Lambda::Function \ --desired-state '{"Code":{"S3Bucket":"cloudcontrol-test","S3Key":"sample_code.zip"},"Role":"arn:aws:iam::123456789012:role/service-role/cloudcontrol-test-role","Runtime":"python3.11","Handler":"lambda_function.lambda_handler"}' \ --client-token f63g2prinrei8pey { "ProgressEvent": { "TypeName": "AWS::Lambda::Function", "RequestToken": "3232173a-7fa7-48d1-90b9-0de15eab75f4", "Operation": "CREATE", "OperationStatus": "IN_PROGRESS", "EventTime": "2023-09-07T00:51:05.678000+00:00" } }
1回目のリクエストはとくに変わりはありません。
$ aws cloudcontrol create-resource \ --type-name AWS::Lambda::Function \ --desired-state '{"Code":{"S3Bucket":"cloudcontrol-test","S3Key":"sample_code.zip"},"Role":"arn:aws:iam::123456789012:role/service-role/cloudcontrol-test-role","Runtime":"python3.11","Handler":"lambda_function.lambda_handler"}' \ --client-token f63g2prinrei8pey { "ProgressEvent": { "TypeName": "AWS::Lambda::Function", "Identifier": "c587rQZAHO1ni2ct4iME79ukb-LaL8mfpQLR6S", "RequestToken": "3232173a-7fa7-48d1-90b9-0de15eab75f4", "Operation": "CREATE", "OperationStatus": "IN_PROGRESS", "EventTime": "2023-09-07T00:51:06.238000+00:00", "RetryAfter": "2023-09-07T00:51:11.238000+00:00" } }
2回目のリクエストの結果を見ると、RequestToken
の値が1回目の値と同じであり、同一リクエストとして処理されていることがわかります。
作成されたLambda関数も関数名c587rQZAHO1ni2ct4iME79ukb-LaL8mfpQLR6S
のみで、複数個作成されることはありませんでした。
このようにClientToken
パラメーターを指定することで冪等性を保証でき、リトライや意図せぬ多重実行の対応などが容易になります。
作成、更新、削除リクエストをする際は
- 常に
ClientToken
を設定する - トークンはUUIDなどのリクエストごとに一意な値になるものを生成する
がベストプラクティスです。
まとめ
統一されたAPIというのは横断的にリソースを扱う際には非常に強力に思えました。
ネックがあるとすれば「操作したいリソースタイプがCloud Control APIをサポートしているか」ということろですが、そこが許されるのであれば積極的に採用していきたいサービスです。
今後もサポートされているリソースタイプが続々追加されていくことを期待したいと思います。