AWS Application Composer と cdk-from-cfn で CDK はノーコード開発できるのか
忙しい人向け
AWS Application Composer (以下、Application Composer) と cdk-from-cfn を組み合わせる事でこんなことができます。
Pos
- 構成図を作るかのような GUI 操作で誰でも簡単に AWS CDK のコードを作成できる
Cons
- 全て L1 コンストラクトで定義されるため、そのままでは AWS CDK 特有の抽象化によるメリットを受けづらい
はじめに
...なにやら見覚えのある記事だと思ったそこのあなた、大正解です。AWS CDK Advent Calendar 2022 でも似た内容の記事を投稿しています。この記事は去年の構成を cdk-from-cfn でリベンジした記事になります。
モチベーション
Application Composer はブラウザベースのキャンバスでドラッグアンドドロップ操作でインフラストラクチャを定義でき、AWS CloudFormation と AWS Serverless Application Model (SAM)のテンプレートとして出力することができるサービスです。一部のブラウザではファイルの同期を行ってくれるため、キャンバス上で操作した結果がリアルタイムにローカルのファイルに反映されます。
...ならば、ファイルの変更を監視して CloudFormation テンプレートから CDK のコードを出力すれば GUI 操作で CDK のコードができあがるのではないか!?という考えが本投稿のモチベーションです。
以下は登場人物の紹介になります。
AWS Application Composer とは
2023 年 3 月 7 日に一般提供を開始した、複数の AWS サービスからサーバーレスアプリケーションを構築するために使用できるビジュアルデザイナーです。Application Composer を使用すると、キャンバス上でサーバーレスリソースをドラッグして、それらを接続することができます。バックグラウンドでは、Application Composer がインフラストラクチャをコードとして AWS CloudFormation テンプレートを生成します。本投稿の主役です。
cdk-from-cfn とは
cdklabs のリポジトリで管理されているパッケージの 1 つで、AWS CloudFormation テンプレートを同等の AWS CDK コードに変換するためのツールです。実は cdk migrate
コマンドの裏側で動くライブラリだったりします。今回は cdk-from-cfn を用いて CDK のコードを出力します。
nodemon
言わずと知れたファイルの変更を検知してくれるツールです。NPM で配布されていて導入が簡単なので利用します。
手順
今回は Application Composer の操作で CDK のコードを生成し、出力された CDK コードをデプロイするところまでをゴールとしてみます。
次の環境で実施しています。
- Windows 11 Pro 22H2
- Node.js v20.9.0
- AWS CDK v2.110.1
- cdk-from-cfn v0.79.0
CDK プロジェクト初期化
以下のコマンドで CDK プロジェクトを初期化します。
npx cdk init app --language typescript
CDK コードを生成する準備
以下のコマンドで nodemon
と cdk-from-cfn
をインストールします。なお、cdk-from-cfn は Application Composer が出力する CloudFormation に含まれる SAM の Transform を解釈できないため、 SAM リポジトリから変換するスクリプトとそれに必要なライブラリもインストールしています。
npm install -D cdk-from-cfn nodemon
pip install aws-sam-translator pyyaml
curl -LO https://raw.githubusercontent.com/aws/serverless-application-model/develop/bin/sam-translate.py
package.json
を編集します。template.yaml
を監視し、変更の度にcdk-from-cfn
を呼び出してlib/my-stack.ts
へ書き込むコマンドをcodegen-mode
として登録します。ちなみに cdk-from-cfn はコマンドラインツールではないため JavaScript から呼び出して利用します。
{
"name": "application-composer-cdk",
"version": "0.1.0",
"bin": {
"application-composer-cdk": "bin/application-composer-cdk.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
- "cdk": "cdk"
+ "cdk": "cdk",
+ "sam-translate": "AWS_DEFAULT_REGION=us-east-1 python sam-translate.py --template-file=sam-template.yaml --output-template=template.json",
+ "codegen": "node -e \"const [cdkfromcfn, fs] = [require('cdk-from-cfn'), require('fs')]; fs.writeFileSync(process.argv[2], cdkfromcfn.transmute(fs.readFileSync(process.argv[1]).toString(), 'typescript', 'MyStack'));\"",
+ "codegen-mode": "nodemon --ignore .aws-composer --ignore template.json --exec \"npm run sam-translate && npm run codegen ./template.json ./lib/application-composer-cdk-stack.ts\" ./sam-template.yaml"
},
"devDependencies": {
"@types/jest": "^29.5.8",
"@types/node": "20.9.0",
"aws-cdk": "2.110.1",
"cdk-from-cfn": "^0.79.0",
"jest": "^29.7.0",
"nodemon": "^3.0.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "~5.2.2"
},
"dependencies": {
"aws-cdk-lib": "2.110.1",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
}
Applicaction Composer でプロジェクトを開く
yaml 形式のテンプレートを用意するためにResources:
だけのファイルを作成します。
echo "Resources:" > sam-template.yaml
先ほどpackage.json
に追加したコマンドを実行しておきましょう。
npm run codegen-mode
次のような CDK コードが作成されるはずです。
import * as cdk from 'aws-cdk-lib';
export interface MyStackProps extends cdk.StackProps {
}
export class MyStack extends cdk.Stack {
public constructor(scope: cdk.App, id: string, props: MyStackProps = {}) {
super(scope, id, props);
// Resources
}
}
次に、Application Composer のサービスページへアクセスし、Create Project
をクリックします。
[メニュー] > [開く] > [プロジェクトフォルダ] を選択します。
下表の通り設定します。
設定項目 | 値 |
---|---|
フォルダを選択 | 先ほど作成した CDK プロジェクトのフォルダ |
テンプレートファイル | sam-template.yaml |
作成
をクリックしたら準備完了です。操作するとリアルタイムで CDK のコードが出力されているのが分かります。
実際には次のようなコードが出力されています。
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions';
export interface MyStackProps extends cdk.StackProps {
}
export class MyStack extends cdk.Stack {
public constructor(scope: cdk.App, id: string, props: MyStackProps = {}) {
super(scope, id, props);
// Resources
const eventRuleToStateMachineRole = new iam.CfnRole(this, 'EventRuleToStateMachineRole', {
assumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: {
Effect: 'Allow',
Principal: {
Service: `events.${this.urlSuffix}`,
},
Action: 'sts:AssumeRole',
Condition: {
ArnLike: {
'aws:SourceArn': `arn:${this.partition}:events:${this.region}:${this.account}:rule/${this.stackName}-EventRule-*`,
},
},
},
},
});
const stateMachineLogGroup = new logs.CfnLogGroup(this, 'StateMachineLogGroup', {
logGroupName: `/aws/vendedlogs/states/${this.stackName}-StateMachine-Logs`,
});
const stateMachineRole = new iam.CfnRole(this, 'StateMachineRole', {
assumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: [
'sts:AssumeRole',
],
Effect: 'Allow',
Principal: {
Service: [
'states.amazonaws.com',
],
},
},
],
},
managedPolicyArns: [
'arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess',
],
policies: [
{
policyName: 'StateMachineRolePolicy1',
policyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'logs:CreateLogDelivery',
'logs:GetLogDelivery',
'logs:UpdateLogDelivery',
'logs:DeleteLogDelivery',
'logs:ListLogDeliveries',
'logs:PutResourcePolicy',
'logs:DescribeResourcePolicies',
'logs:DescribeLogGroups',
],
Resource: '*',
},
],
},
},
],
tags: [
{
key: 'stateMachine:createdBy',
value: 'SAM',
},
],
});
if (stateMachineLogGroup == null) { throw new Error(`A combination of conditions caused 'stateMachineLogGroup' to be undefined. Fixit.`); }
if (stateMachineRole == null) { throw new Error(`A combination of conditions caused 'stateMachineRole' to be undefined. Fixit.`); }
const stateMachine = new stepfunctions.CfnStateMachine(this, 'StateMachine', {
definitionString: [
'{',
' \"StartAt\": \"Start\",',
' \"States\": {',
' \"Done\": {',
' \"End\": true,',
' \"Type\": \"Pass\"',
' },',
' \"Start\": {',
' \"Next\": \"Done\",',
' \"Type\": \"Pass\"',
' }',
' }',
'}',
].join('\n'),
loggingConfiguration: {
level: 'ALL',
includeExecutionData: true,
destinations: [
{
cloudWatchLogsLogGroup: {
logGroupArn: stateMachineLogGroup.attrArn,
},
},
],
},
roleArn: stateMachineRole.attrArn,
stateMachineType: 'STANDARD',
tags: [
{
key: 'stateMachine:createdBy',
value: 'SAM',
},
],
tracingConfiguration: {
enabled: true,
},
});
if (eventRuleToStateMachineRole == null) { throw new Error(`A combination of conditions caused 'eventRuleToStateMachineRole' to be undefined. Fixit.`); }
if (stateMachine == null) { throw new Error(`A combination of conditions caused 'stateMachine' to be undefined. Fixit.`); }
const eventRule = new events.CfnRule(this, 'EventRule', {
eventPattern: {
source: [
'aws.health',
],
},
targets: [
{
id: stateMachine.attrName,
arn: stateMachine.ref,
roleArn: eventRuleToStateMachineRole.attrArn,
},
],
});
if (eventRuleToStateMachineRole == null) { throw new Error(`A combination of conditions caused 'eventRuleToStateMachineRole' to be undefined. Fixit.`); }
if (stateMachine == null) { throw new Error(`A combination of conditions caused 'stateMachine' to be undefined. Fixit.`); }
const eventRuleToStateMachineConnector = new iam.CfnManagedPolicy(this, 'EventRuleToStateMachineConnector', {
policyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'states:StartExecution',
],
Resource: [
stateMachine.ref,
],
},
],
Version: '2012-10-17',
},
roles: [
eventRuleToStateMachineRole.ref,
],
});
}
}
変換された CDK アプリケーションをデプロイしてみます。
npx cdk deploy
無事デプロイすることができました。
おまけ
L1 コンストラクトだけでは AWS CDK の抽象化による恩恵を受けづらいので、簡単に L2 に変換できないか Amazon Bedrock の Claude v2.1 を利用して検証してみました。次のようなプロンプトで実行します。
次のコードが返ってきました。
import * as cdk from "aws-cdk-lib";
import * as events from "aws-cdk-lib/aws-events";
import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs";
import * as stepfunctions from "aws-cdk-lib/aws-stepfunctions";
export class MyStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Resources
const eventRuleRole = new iam.Role(this, "EventRuleRole", {
assumedBy: new iam.ServicePrincipal("events.amazonaws.com"),
});
const logGroup = new logs.LogGroup(this, "StateMachineLogs");
const stateMachineRole = new iam.Role(this, "StateMachineRole", {
assumedBy: new iam.ServicePrincipal("states.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName("AWSXrayWriteOnlyAccess"),
],
});
stateMachineRole.addToPolicy(
new iam.PolicyStatement({
actions: ["logs:*"],
resources: ["*"],
})
);
const stateMachine = new stepfunctions.StateMachine(this, "StateMachine", {
logging: {
level: stepfunctions.LogLevel.ALL,
includeExecutionData: true,
destinations: [new stepfunctions.LogDestination(logGroup)],
},
role: stateMachineRole,
tracingEnabled: true,
definition: stepfunctions.Chain.start(
new stepfunctions.Pass(this, "Start")
).next(new stepfunctions.Pass(this, "Done")),
});
stateMachine.addToRolePolicy(
new iam.PolicyStatement({
actions: ["states:StartExecution"],
resources: [stateMachine.stateMachineArn],
})
);
const eventRule = new events.Rule(this, "EventRule", {
targets: [new targets.SfnStateMachine(stateMachine)],
eventPattern: {
source: ["aws.health"],
},
});
eventRule.grantStartExecution(stateMachine);
}
}
一部誤った設定があるようですが、ある程度は L2 に作り直してくれそうですね。
さいごに
去年使った cdk-dasm と違って CloudFormation 組み込み関数も処理してくれたり、リソース間の参照も文字列参照ではなく CDK LIKE な書き方になっていて実用的になったような気がします。cdk-from-cfn もまだまだ積極的に開発されているので今後に期待です。
Discussion