diff --git a/README.md b/README.md index 7f79ab9..f5f4bf8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ The parameters for the Cloudformation template can be managed per environment an for instance Java properties files. Values in the Java properties files will be "interpolated" according to Groovy's evaluation. That is, properties can reference other properties and Groovy/Java functions. +The plugin also has utilities to do stack resource lookups. I.e. lookup a stack's logical resource to get its +physical resource ID. + ## Usage @@ -13,7 +16,7 @@ Apply the plugin to your project. ```groovy plugins { - id 'se.solrike.cloudformation' version '1.0.0-beta.3' + id 'se.solrike.cloudformation' version '1.0.0' } ``` Gradle 7.0 or later must be used. @@ -24,7 +27,7 @@ The tasks have to be created. Minimal example on how to create a task that creat ```groovy plugins { - id 'se.solrike.cloudformation' version '1.0.0-beta.3' + id 'se.solrike.cloudformation' version '1.0.0' } task deployS3Stack(type: se.solrike.cloudformation.CreateOrUpdateStackTask) { parameters = [ s3BucketName : 's3-bucket4711'] @@ -49,7 +52,7 @@ Resources: AWS credentials needs to be configured. E.g. environment variables or using `aws configure` CLI or Java system properties. -## The plugin provides three task +## The plugin provides three tasks and some utils methods The plugin provides three tasks that all need to be created manually. They will not be created when the plugin is applied. @@ -57,6 +60,11 @@ applied. * DeleteStackTask * PrintEnviromentParametersTask +Utility methods: + +* se.solrike.cloudformation.StackUtils.resolveStackResource(...) +* se.solrike.cloudformation.StackUtils.resolveStackOutput(...) + ## How the CreateOrUpdateStackTask works The usual credential and region chain is used to find the credentials and the region to use. @@ -84,7 +92,7 @@ The task will delete a stack. Typically define a task like this: ```groovy task deleteS3Stack(type: se.solrike.cloudformation.DeleteStackTask) { - stackName = "s3-buckets" + stackName = 's3-buckets' } ``` @@ -106,6 +114,37 @@ task printEnv(type: se.solrike.cloudformation.PrintEnviromentParametersTask) { } ``` +## How the StackUtils.resolveStackResource(...) works +The method will resolve a AWS Cloudformation stack logical resource ID to a physical resource ID. + +In a complex setup it is sometimes required to pass resource IDs from one stack to another. One what is to import the value from the dependent stack. But sometimes it might not be possible. + +E.g. + +``` +task lookupStackResource { + doLast { + println se.solrike.cloudformation.StackUtils.resolveStackResource( + 'my-stack-name', 'ServerlessFunctionApiAnyEventPermission') + } +} +``` + +## How the StackUtils.resolveStackOutput(...) works +The method will resolve an output of a stack using the output's key. + +In a complex setup it is sometimes required to pass outputs from one stack to another. One what is to import the value from the dependent stack. But sometimes it might not be possible. + +E.g. + +``` +task lookupStackOutput { + doLast { + println se.solrike.cloudformation.StackUtils.resolveStackOutput( + 'my-stack-name', 'LogGroupName') + } +} +``` ## More advanced example ### Example 1 @@ -254,8 +293,9 @@ task deployS3Stack(type: se.solrike.cloudformation.CreateOrUpdateStackTask) { ## Release notes -### 1.0.0-beta.3 -Then the stack is ready any outputs form the stack will be listed on the console. +### 1.0.0 +Then the stack is ready any outputs from the stack will be listed on the console. +New utils function to fetch an ID of stack's resource. I.e. translate the logical ID to a physical ID. ### 1.0.0-beta.2 * Supports Groovy string interpolation of values for the stack parameters. diff --git a/build.gradle b/build.gradle index 984e163..8839ed0 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,9 @@ repositories { dependencies { implementation 'org.ajoberstar.grgit:grgit-core:5.0.0' - def awsSdkVersion = '2.17.292' + def awsSdkVersion = '2.18.24' implementation "software.amazon.awssdk:cloudformation:$awsSdkVersion" + implementation "software.amazon.awssdk:ssm:$awsSdkVersion" testImplementation('org.assertj:assertj-core:3.22.0') testImplementation('org.junit.jupiter:junit-jupiter') @@ -19,7 +20,7 @@ dependencies { } group = 'se.solrike.cloudformation' -version = '1.0.0-beta.3' +version = '1.0.0' java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -108,7 +109,8 @@ pluginBundle { pluginTags = [ cloudformationPlugin: [ 'Cloudformation', - 'AWS' + 'AWS', + 'continues deployment' ] ] } diff --git a/src/main/groovy/se/solrike/cloudformation/CreateOrUpdateStackTask.groovy b/src/main/groovy/se/solrike/cloudformation/CreateOrUpdateStackTask.groovy index 864a796..e240a5b 100644 --- a/src/main/groovy/se/solrike/cloudformation/CreateOrUpdateStackTask.groovy +++ b/src/main/groovy/se/solrike/cloudformation/CreateOrUpdateStackTask.groovy @@ -102,10 +102,10 @@ abstract class CreateOrUpdateStackTask extends DefaultTask { println "Stack '${getStackName().get()}' is ready." // Check if the stack has any outputs defined that can be listed. - DescribeStacksResponse stack = client.describeStacks { + DescribeStacksResponse response = client.describeStacks { it.stackName(getStackName().get()) } - stack.stacks().get(0).outputs().forEach({output -> println "$output.outputKey : $output.outputValue"}) + response.stacks().get(0).outputs().forEach({output -> println "$output.outputKey : $output.outputValue"}) } void createStack() { @@ -126,8 +126,6 @@ abstract class CreateOrUpdateStackTask extends DefaultTask { WaiterResponse waiterResponse = client.waiter().waitUntilStackCreateComplete { it.stackName(getStackName().get()) } - // def outputs = waiterResponse.matched().response().get().stacks().get(0).outputs() - // println outputs } void updateStack() { @@ -164,7 +162,6 @@ abstract class CreateOrUpdateStackTask extends DefaultTask { } // read the content of the template file and return it as a big string - // TODO check the size of the string and fail if it is to big? But AWS will fail it anyway. String createTemplateBody() { return new File(getTemplateFileName().get()).text } diff --git a/src/main/groovy/se/solrike/cloudformation/SsmUtils.groovy b/src/main/groovy/se/solrike/cloudformation/SsmUtils.groovy new file mode 100644 index 0000000..557d282 --- /dev/null +++ b/src/main/groovy/se/solrike/cloudformation/SsmUtils.groovy @@ -0,0 +1,46 @@ +package se.solrike.cloudformation + +import javax.annotation.Nullable + +import software.amazon.awssdk.services.ssm.SsmClient +import software.amazon.awssdk.services.ssm.model.ParameterType +import software.amazon.awssdk.services.ssm.paginators.GetParametersByPathIterable + +/** + * Utilities for AWS SSM (Simple system management) like Parameter Store + */ +public class SsmUtils { + + public static void uploadParameters(Map parameters, String excludeFilter, + @Nullable String parameterPrefix = null) { + SsmClient client = SsmClient.builder().build() + + parameters.findAll { !it.key.startsWith(excludeFilter) && it.value }.each { name, value -> + client.putParameter { + it.name(parameterPrefix ? "/$parameterPrefix/$name" : "/$name") + it.value(value) + it.overwrite(true) + it.type(encryptParameter(name) ? ParameterType.SECURE_STRING : ParameterType.STRING) + } + } + } + + public static void printParameters(String parameterPrefix) { + SsmClient client = SsmClient.builder().build() + List parameters = [] + + GetParametersByPathIterable iterable = client.getParametersByPathPaginator { + it.path('/' + parameterPrefix) + it.withDecryption(true) + } + iterable.forEach { + it.parameters.each { parameters.add("$it.name : $it.value") } + } + parameters.sort().each { println it} + } + + + static boolean encryptParameter(String name) { + return (name.endsWithIgnoreCase('password') || name.endsWithIgnoreCase('key')) + } +} diff --git a/src/main/groovy/se/solrike/cloudformation/StackUtils.groovy b/src/main/groovy/se/solrike/cloudformation/StackUtils.groovy new file mode 100644 index 0000000..fafb8f4 --- /dev/null +++ b/src/main/groovy/se/solrike/cloudformation/StackUtils.groovy @@ -0,0 +1,55 @@ +package se.solrike.cloudformation + + +import software.amazon.awssdk.services.cloudformation.CloudFormationClient +import software.amazon.awssdk.services.cloudformation.model.DescribeStackResourceResponse +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse +import software.amazon.awssdk.services.cloudformation.model.Output + +/** + * Utilities for interact with AWS. E.g. Cloudformation stacks + */ +public class StackUtils { + + /** + * Resolve a AWS Cloudformation stack logical resource ID to a physical resource ID + * + * @param stackName - stackname , e.g. 'my-lambda-stack-for-production' + * @param logicalResourceId - logical resource ID in the stack, e.g. 'ServerlessFunctionArn' + * @return physical resource ID, e.g. 'arn:aws:lambda:eu-north-1:12345:function:slrk-prod-books-api' + */ + public static String resolveStackResource(String stackName, String logicalResourceId) { + CloudFormationClient client = CloudFormationClient.builder().build() + + DescribeStackResourceResponse response = client.describeStackResource { + it.stackName(stackName) + it.logicalResourceId(logicalResourceId) + } + return response.stackResourceDetail.physicalResourceId + } + + /** + * Resolve an output of a stack using the output's key. + * + * @param stackName - stackname , e.g. 'my-lambda-stack-for-production' + * @param key - e.g. 'LogGroupName' + * @return the value for the key, e.g. '/aws/lambda/slrk-prod-books-api' + * + * @throws CloudFormationException - e.g. if the stack doesn't exists. + * @throws IllegalArgumentException - if the key doesn't exists. + */ + public static String resolveStackOutput(String stackName, String key) { + CloudFormationClient client = CloudFormationClient.builder().build() + + DescribeStacksResponse response = client.describeStacks { + it.stackName(stackName) + } + Output output = response.stacks().get(0).outputs().find { it.outputKey == key } + if (output) { + return output.outputValue + } + else { + throw new IllegalArgumentException("Cloud not find output with key '$key'.") + } + } +}