jill64 / sveltekit-adapter-aws

🔌 SveleteKit AWS adapter with multiple architecture
https://npmjs.com/package/@jill64/sveltekit-adapter-aws
MIT License
21 stars 4 forks source link

Best approach to modify CDK to use additional services #481

Open hiddengearz opened 4 months ago

hiddengearz commented 4 months ago

Hello,

Svelte & cloudformation noob here, I've been working on an demo app that uses AWS bedrock and cognito. However after using this adapter I realized it generates a cloudformation template and not the CDK code, so I'm a bit stuck on the best way to incorporate bedrock and cognito.

What would you suggest?

hiddengearz commented 4 months ago

TIL you can import cloudformation into CDK, seems like this may be my best bet as I'm far more familiar with CDK.

Would still love to hear others opinions!

hiddengearz commented 4 months ago

Importing the cloudformation CDK does work, though because the names of the constructs like S3 buckets and etc change everytime you'll have to play with regex if you need to make any modifications to them e.g assign roles.

It honestly may be easier to fork this repo and make changes to the original cdk in sveltekit-adapter-aws/cdk/arch.

Here is an example if anyone decides to go down this route:

export class SveleteStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, {...props,analyticsReporting: false} );

    const filePath = path.resolve(__dirname, '../../build/cdk.out/faulty-bedrock.template.json');
    //Get template from Svelete build
    const template = new cfninc.CfnInclude(this, 'Template', { 
      templateFile: filePath,
    });

    const [lambdaName, s3BucketName, cloudDistributionName] =  getCFResources();

    //get lambda that was genereated by Svelete
    const cfnLambda = template.getResource(lambdaName) as lambda.CfnFunction;
    const lambdaFunc = lambda.Function.fromFunctionName(this, lambdaName, cfnLambda.ref);

    //create bedrock policy
    const bedrockAccessPolicy = new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], // See: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html
      resources: ["arn:aws:bedrock:*::foundation-model/anthropic.claude-v2"],
    });

    //Add these policies to the role used by that lambda
    lambdaFunc.addToRolePolicy(bedrockAccessPolicy);
hiddengearz commented 4 months ago

Ultimately ended up just copying the cdk from the adapter and modifying it. The above method seems nice but is too much of a headache, likely caused some of the issues I experienced in #485

jill64 commented 4 months ago

It is believed that only manual resource creation and integration is being used at the moment. However, integration with other AWS resources is an important issue. I would like to add an optional CDK extension function that takes the resource created by the adapter as an argument.

// svelte.config.js
{
  // ...
  adapter: adapter({
    // ...
    architecture: 'lambda-s3',
    extends: (lambda, s3, cloudfront) => {
      new aws.cognito.UserPoolClient(this, 'UserPoolClient', {
        userPool: 'UserPool',
        generateSecret: false,
        userPoolClientName: 'UserPoolClient'
        // ...
      })

      // ...
    }
  })
}

Naturally, they should be separated into separate files. We will also provide the necessary types.

// svelte.config.js
import { extendsCDK } from './extendsCDK.js'

{
  // ...
  adapter: adapter({
    // ...
    architecture: 'lambda-s3',
    extends: extendsCDK
  })
}
// extendsCDK.ts
import type { ExtendsLambdaS3 } from '@jill64/sveltekit-adapter-aws/types'

export const extendsCDK: ExtendsLambdaS3 = (lambda, s3, cloudfront) => {
  new aws.cognito.UserPoolClient(this, 'UserPoolClient', {
    userPool: 'UserPool',
    generateSecret: false,
    userPoolClientName: 'UserPoolClient'
    // ...
  })

  // ...
}

I have considered merging CloudFormation templates, but it seems a bit too challenging for me. What do you think about this idea?

hiddengearz commented 4 months ago

I think the above would work well, especially for simple stacks.

The main issue I ran into (which I believe this method will run into aswell) is that it seems to be far more difficult to make modifications to a stack/cloud formation template, from another stack. I had to make changes to the constructs in the generated stack and trying to do that from another stack, was annoying and ultimately didn't work. I'm not a pro but that is my experience from my attempt above.

For advanced stacks If I was able to modify the generated stack and it not be overwritten every time I run pnpm run build that would be sufficient.

I'll upload the two CDK's I made as an example, both should of done basically the same thing (sorry for the messy code, I wasn't planning to post this publicly anytime soon)

Attempt 1 - Importing a cloudformation template I believe if you go the route of using 2 stacks you'll ultimately end up importing two CDK's or cloud formation templates. Interactions between them is difficult (from my limited experience) ```ts import * as cdk from 'aws-cdk-lib'; import * as cfninc from 'aws-cdk-lib/cloudformation-include'; import * as lambda from "aws-cdk-lib/aws-lambda"; import { Construct } from 'constructs'; import { PolicyStatement, Effect, Role, ServicePrincipal, } from "aws-cdk-lib/aws-iam"; import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as cr from 'aws-cdk-lib/custom-resources'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as path from 'path'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as fs from 'fs'; import * as cd from 'aws-cdk-lib/aws-cloudfront'; export class SveleteStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, {...props,analyticsReporting: false} ); const filePath = path.resolve(__dirname, '../../build/cdk.out/faulty-bedrock.template.json'); //Get template from Svelete build const template = new cfninc.CfnInclude(this, 'Template', { templateFile: filePath, }); const mySecret = new secretsmanager.Secret(this, 'MySecret', { secretName: 'AUTH_SECRET', generateSecretString: { passwordLength: 32, excludePunctuation: true, }, }); const [lambdaName, s3BucketName, cloudDistributionName, cdkDeploymentaName] = getCFResources(); //get CustomCDKBucketDeployment lambda that was genereated by Svelete const cfnCdkLambda = template.getResource(cdkDeploymentaName) as lambda.CfnFunction; const cdklambdaFunc = lambda.Function.fromFunctionName(this, cdkDeploymentaName, cfnCdkLambda.ref); //get server lambda that was genereated by Svelete const cfnLambda = template.getResource(lambdaName) as lambda.CfnFunction; const lambdaFunc = lambda.Function.fromFunctionName(this, lambdaName, cfnLambda.ref); cfnLambda.addDependency(cfnCdkLambda); //get S3 that was genereated by Svelete const cfnBucket = template.getResource(s3BucketName) as s3.CfnBucket; const bucket = s3.Bucket.fromBucketName(this, s3BucketName, cfnBucket.ref); cfnBucket.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); //get S3 that was genereated by Svelete const cfnCloudDistribution = template.getResource(cloudDistributionName) as cd.CfnDistribution; const callBackURL:string = "https://" + cfnCloudDistribution.attrDomainName + "/api/auth/callback/cognito" console.log("call back URL:", callBackURL); const userpool = new cognito.UserPool(this, 'user-pool', { signInAliases: { email: true, }, selfSignUpEnabled: true, standardAttributes: { familyName: { mutable: false, required: false, }, address: { mutable: true, required: false, }, }, customAttributes: { 'createdAt': new cognito.DateTimeAttribute(), 'isAdmin': new cognito.BooleanAttribute({ mutable: false, }), }, passwordPolicy: { minLength: 8, requireLowercase: true, requireUppercase: true, requireDigits: true, requireSymbols: false, }, accountRecovery: cognito.AccountRecovery.EMAIL_ONLY, removalPolicy: cdk.RemovalPolicy.DESTROY, }); const appClient = userpool.addClient('app-client', { userPoolClientName: 'app-client', authFlows: { userPassword: true, }, generateSecret: true, oAuth: { callbackUrls: [ callBackURL, ], }, }); //Cognito policy const cognitoPolicy = new PolicyStatement({ effect: Effect.ALLOW, actions: ["cognito-idp:SignUp", "cognito-idp:InitiateAuth"], // See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncognitouserpools.html //resources: ["arn:aws:cognito-idp:*::userpool/YOUR_USER_POOL_ID"], resources: ["*"], }); //create bedrock policy const bedrockAccessPolicy = new PolicyStatement({ effect: Effect.ALLOW, actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], // See: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html resources: ["arn:aws:bedrock:*::foundation-model/anthropic.claude-v2"], }); //Add these policies to the role used by that lambda lambdaFunc.addToRolePolicy(bedrockAccessPolicy); lambdaFunc.addToRolePolicy(cognitoPolicy); // Grant read permissions to the Lambda function's execution role lambdaFunc.addToRolePolicy(new PolicyStatement({ actions: ['s3:GetObject', "s3:ListBucket"], resources: ["*"], })); console.log("bucket arn:", bucket.bucketArn); // Define the Lambda function to update environment variables const updateEnvVarsLambda = new lambda.Function(this, 'UpdateEnvVarsLambda', { runtime: lambda.Runtime.NODEJS_18_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = async (event) => {}'), }); // Grant necessary permissions to the updateEnvVarsLambda function updateEnvVarsLambda.addToRolePolicy(new PolicyStatement({ actions: ['lambda:UpdateFunctionConfiguration'], resources: ['*'], // Replace with the ARN of the target Lambda function if more restrictive permissions are desired })); // Grant read permissions to the Lambda function's execution role updateEnvVarsLambda.addToRolePolicy(new PolicyStatement({ actions: ['s3:GetObject'], resources: [bucket.arnForObjects('*')], })); console.log("bucket arn:", bucket.bucketArn); // Define the CloudFormation custom resource to trigger the Lambda function const updateEnvVars = new cr.AwsCustomResource(this, 'UpdateEnvVarsCustomResource', { onCreate: { service: 'Lambda', action: 'updateFunctionConfiguration', parameters: { FunctionName: lambdaFunc.functionName, // Replace with the name of the existing Lambda function Environment: { Variables: { COGNITO_USER_POOL_ID: userpool.userPoolId, COGNITO_CLIENT_ID: appClient.userPoolClientId, COGNITO_CLIENT_SECRET: appClient.userPoolClientSecret.unsafeUnwrap(), COGNITO_ISSUER: "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId, AUTH_TRUST_HOST: "true", AUTH_SECRET: mySecret.secretValue.unsafeUnwrap() //should use secrets manager TODO }, }, }, physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), }, onUpdate: { service: 'Lambda', action: 'updateFunctionConfiguration', parameters: { FunctionName: lambdaFunc.functionName, // Replace with the name of the existing Lambda function Environment: { Variables: { COGNITO_USER_POOL_ID: userpool.userPoolId, COGNITO_CLIENT_ID: appClient.userPoolClientId, COGNITO_CLIENT_SECRET: appClient.userPoolClientSecret.unsafeUnwrap(), //TODO: Bad practice, ctf app so not critical COGNITO_ISSUER: "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId, AUTH_TRUST_HOST: "true", //AUTH_SECRET: mySecret.secretValue.unsafeUnwrap(), //TODO: Bad practice, ctf app so not critical AUTH_SECRET:"efb724599037a553a63ccf4aa24ce4f847b4eddff8ecf99da641379fcd924c36" }, }, }, physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), }, policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE }), }); updateEnvVars.node.addDependency(cfnCdkLambda); // Loop through all constructs in the stack template.node.findAll().forEach(construct => { if (construct.node.id !== cdkDeploymentaName){ construct.node.addDependency(cdklambdaFunc); } }); this.node.findAll().forEach(construct => { if (construct.node.id !== cdkDeploymentaName){ construct.node.addDependency(cdklambdaFunc); } }); } } // Function to find the first resource name that matches the specified criteria function findResourceName(regex: RegExp, template: string, namePrefix: string): string{ const match: RegExpMatchArray | null = template.match(regex); if (match) { return match[1].toString(); } throw new Error(`Resource starting with prefix '${namePrefix}' not found`); } function getCFResources():[string, string, string, string] { // Load CloudFormation template file const filePath = path.resolve(__dirname, '../../build/cdk.out/faulty-bedrock.template.json'); const template: string = fs.readFileSync(filePath, 'utf-8'); // Regular expressions for matching Lambda function name and S3 bucket name const lambdaRegex: RegExp = /"(Server[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::Lambda::Function"/i; const s3Regex: RegExp = /"(Bucket[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::S3::Bucket"/i; const cdRegex: RegExp = /"(CloudFront[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::CloudFront::Distribution"/i; const cdkDeploymentRex: RegExp = /"(CustomCDKBucketDeployment[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::Lambda::Function"/i; // Find Lambda function name and S3 bucket name const lambdaName: string = findResourceName(lambdaRegex, template, "server"); const s3BucketName: string = findResourceName(s3Regex, template, "cdk"); const cloudDistributionName: string = findResourceName(cdRegex, template, "cdk"); const cdkDeploymentaName: string = findResourceName(cdkDeploymentRex, template, "cdkDeployment"); console.log("Lambda function name:", lambdaName); console.log("S3 bucket name:", s3BucketName); console.log("cloud Distribution name:", cloudDistributionName); console.log("CDK Deployment function name:", cdkDeploymentaName); return [lambdaName, s3BucketName, cloudDistributionName, cdkDeploymentaName] } ```
Attempt 2 - Modifying the generated CDK ```ts import { CfnOutput, Duration, Fn, Stack, StackProps, aws_certificatemanager, aws_cloudfront, aws_cloudfront_origins, aws_lambda, aws_s3, aws_s3_deployment, aws_cognito, custom_resources, aws_secretsmanager, aws_iam, RemovalPolicy } from 'aws-cdk-lib' import { Construct } from 'constructs' import { appPath, bridgeAuthToken, certificateArn, domainName, environment, memorySize } from '../../build/external/params'; import * as path from 'path'; import * as lambda from "aws-cdk-lib/aws-lambda"; export class CDKStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props) //Start of new code const buildDir = path.resolve(__dirname, '../../build/'); const mySecret = new aws_secretsmanager.Secret(this, 'MySecret', { secretName: 'AUTH_SECRET', generateSecretString: { passwordLength: 32, excludePunctuation: true, }, }); const cognitoDomain = generateRandomString(12); const userpool = new aws_cognito.UserPool(this, 'user-pool', { signInAliases: { email: true, }, selfSignUpEnabled: true, standardAttributes: { familyName: { mutable: false, required: false, }, address: { mutable: true, required: false, }, }, customAttributes: { 'createdAt': new aws_cognito.DateTimeAttribute(), 'isAdmin': new aws_cognito.BooleanAttribute({ mutable: false, }), }, passwordPolicy: { minLength: 8, requireLowercase: true, requireUppercase: true, requireDigits: true, requireSymbols: false, }, accountRecovery: aws_cognito.AccountRecovery.EMAIL_ONLY, removalPolicy: RemovalPolicy.DESTROY, }); // Check if the user pool already has a domain configured const existingDomain = userpool.node.tryFindChild('MyUserPoolDomain') as aws_cognito.CfnUserPoolDomain | undefined; // If the domain doesn't exist, add it to the user pool if (!existingDomain) { userpool.addDomain('MyCognitoDomain',{ cognitoDomain: { domainPrefix: cognitoDomain }, }); } const lambdaPolicy = new aws_iam.PolicyDocument({ statements: [ // Allow Lambda to invoke Cognito new aws_iam.PolicyStatement({ actions: [ //'cognito-idp:AdminCreateUser', //'cognito-idp:AdminUpdateUserAttributes', "cognito-idp:SignUp", "cognito-idp:InitiateAuth", "cognito-idp:RespondToAuthChallenge", "cognito-idp:ConfirmSignUp", "cognito-idp:GlobalSignOut", "cognito-idp:GetUser", "cognito-idp:UpdateUserAttributes", "cognito-idp:ForgotPassword", "cognito-idp:ConfirmForgotPassword" // See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncognitouserpools.html ], resources: ['*'], // Be cautious with using '*' as it grants access to all resources }), // Allow Lambda to invoke Bedrock new aws_iam.PolicyStatement({ actions: [ "bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", ], // See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncognitouserpools.html //resources: ["arn:aws:cognito-idp:*::userpool/YOUR_USER_POOL_ID"], resources: ['*'], //TODO lockdown to claude model Replace '*' with the ARN of the Bedrock resource if possible }), new aws_iam.PolicyStatement({ actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], resources: ['arn:aws:logs:*:*:*'], }), ], }); // Step 2: Create an IAM role for the Lambda function const lambdaRole = new aws_iam.Role(this, 'LambdaRole', { assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'), }); // Step 3: Attach the IAM policy to the IAM role lambdaRole.attachInlinePolicy(new aws_iam.Policy(this, 'LambdaPolicy', { document: lambdaPolicy })); //end of new code const lambdaFunc = new aws_lambda.Function(this, 'Server', { runtime: aws_lambda.Runtime.NODEJS_18_X, code: aws_lambda.Code.fromAsset(buildDir.toString() + '/lambda'), handler: 'server.handler', architecture: aws_lambda.Architecture.ARM_64, memorySize, timeout: Duration.seconds(30), environment, role: lambdaRole }); const lambdaURL = lambdaFunc.addFunctionUrl({ authType: aws_lambda.FunctionUrlAuthType.NONE, invokeMode: aws_lambda.InvokeMode.RESPONSE_STREAM }) const certificate = certificateArn ? aws_certificatemanager.Certificate.fromCertificateArn( this, 'CertificateManagerCertificate', certificateArn ) : undefined const s3 = new aws_s3.Bucket(this, 'Bucket', { transferAcceleration: true }) const cf2 = new aws_cloudfront.Function(this, 'CF2', { code: aws_cloudfront.FunctionCode.fromFile({ filePath: buildDir.toString() +'/cf2/index.js' }) }) const behaviorBase = { viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, originRequestPolicy: aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, functionAssociations: [ { function: cf2, eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST } ] } const cdn = new aws_cloudfront.Distribution(this, 'CloudFront', { domainNames: domainName ? [domainName] : undefined, certificate, defaultBehavior: { ...behaviorBase, allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: aws_cloudfront.CachePolicy.CACHING_DISABLED, origin: new aws_cloudfront_origins.HttpOrigin( Fn.select(2, Fn.split('/', lambdaURL.url)), { protocolPolicy: aws_cloudfront.OriginProtocolPolicy.HTTPS_ONLY, originSslProtocols: [aws_cloudfront.OriginSslPolicy.TLS_V1_2], customHeaders: { 'Bridge-Authorization': `Plain ${bridgeAuthToken}` } } ) }, httpVersion: aws_cloudfront.HttpVersion.HTTP2_AND_3, additionalBehaviors: { [appPath]: { ...behaviorBase, origin: new aws_cloudfront_origins.S3Origin(s3) } } }) new aws_s3_deployment.BucketDeployment(this, 'S3Deploy', { sources: [aws_s3_deployment.Source.asset(buildDir.toString() + '/s3')], destinationBucket: s3, distribution: cdn }) const cdnName: string = "https://" + cdn.domainName ?? cdn.distributionDomainName; console.log("cdnName = " + cdnName); const appClient = userpool.addClient('app-client', { userPoolClientName: 'app-client', authFlows: { userPassword: true, }, generateSecret: true, oAuth: { callbackUrls: [ cdnName + "/auth/callback/cognito", cdnName + ":5173/auth/callback/cognito", cdnName + ":5000/auth/callback/cognito", cdnName + ":3000/auth/callback/cognito", 'http://localhost:5173/auth/callback/cognito', 'https://localhost:5173/auth/callback/cognito', 'http://localhost:5000/auth/callback/cognito', 'https://localhost:5000/auth/callback/cognito', 'http://localhost:3000/auth/callback/cognito', 'https://localhost:3000/auth/callback/cognito', ], flows: { authorizationCodeGrant: true, } }, }); //Can't use this because it creates a circular dependency, have to use the "UpdateEnvVarsLambda" as a workaround /* lambdaFunc.addEnvironment("COGNITO_USER_POOL_ID", userpool.userPoolId); lambdaFunc.addEnvironment("COGNITO_CLIENT_ID", appClient.userPoolClientId); lambdaFunc.addEnvironment("COGNITO_CLIENT_SECRET", appClient.userPoolClientSecret.unsafeUnwrap()); lambdaFunc.addEnvironment("COGNITO_ISSUER", "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId); lambdaFunc.addEnvironment("AUTH_TRUST_HOST", "true"); lambdaFunc.addEnvironment("AUTH_SECRET", "12344567890"); //lambdaFunc.addEnvironment("AUTH_SECRET", mySecret.secretValue.unsafeUnwrap()); //TODO: Bad practice, ctf app so not critical */ // Define the Lambda function to update environment variables const updateEnvVarsLambda = new lambda.Function(this, 'UpdateEnvVarsLambda', { runtime: lambda.Runtime.NODEJS_18_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = async (event) => {}'), }); // Grant necessary permissions to the updateEnvVarsLambda function updateEnvVarsLambda.addToRolePolicy(new aws_iam.PolicyStatement({ actions: ['lambda:UpdateFunctionConfiguration'], resources: ['*'], // Replace with the ARN of the target Lambda function if more restrictive permissions are desired })); const environmentVariables: { [key: string]: string } = { "COGNITO_USER_POOL_ID": userpool.userPoolId, "COGNITO_CLIENT_ID": appClient.userPoolClientId, "COGNITO_CLIENT_SECRET": appClient.userPoolClientSecret.unsafeUnwrap(), "COGNITO_ISSUER": "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId, "AUTH_TRUST_HOST": "true", "AUTH_SECRET": "
hiddengearz commented 4 months ago

In hindsight for attempt 1 I'm pretty sure I was using the wrong names to retrieve the constructs from the cloudformation template, so there would be no need for findResourceName and it probably would of worked... but I think this shows the difference between the approaches.

b9n2038 commented 1 week ago

I think you would want to extend via CDK. Have some way to allow the adapter to inject other stacks / files into the one cdk application or maybe allow specification of an alternative cdk app file that can import the Svelte Stack

Also there are easy ways to share resources across stacks, you can export and import resources from separate stacks using CfnOutput and Fn.ImportValue. If you haven't tried this before this works pretty well except for:

  1. Stack dependencies are painful if you want to refactor/rename exports as you can't rename an export if it is imported by another stack, you'll have to drop the dependency first;
  2. The interface values from the import (eg. IBucket vs. Bucket) have constraints and I sometimes need the concrete class, in which case I'm re-factoring constructs/stacks.

cdk deploy can output json with output values for use in other build process.