Skip to content

Commit

Permalink
add support for AWS SSM
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas3oo committed Nov 27, 2022
1 parent a894ad8 commit d9f26d1
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 14 deletions.
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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']
Expand All @@ -49,14 +52,19 @@ 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.

* CreateOrUpdateStackTask
* 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.

Expand Down Expand Up @@ -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'
}
```

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ 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')
testImplementation(platform('org.junit:junit-bom:5.8.2'))
}

group = 'se.solrike.cloudformation'
version = '1.0.0-beta.3'
version = '1.0.0'
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
Expand Down Expand Up @@ -108,7 +109,8 @@ pluginBundle {
pluginTags = [
cloudformationPlugin: [
'Cloudformation',
'AWS'
'AWS',
'continues deployment'
]
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -126,8 +126,6 @@ abstract class CreateOrUpdateStackTask extends DefaultTask {
WaiterResponse<DescribeStacksResponse> waiterResponse = client.waiter().waitUntilStackCreateComplete {
it.stackName(getStackName().get())
}
// def outputs = waiterResponse.matched().response().get().stacks().get(0).outputs()
// println outputs
}

void updateStack() {
Expand Down Expand Up @@ -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
}
Expand Down
46 changes: 46 additions & 0 deletions src/main/groovy/se/solrike/cloudformation/SsmUtils.groovy
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String> 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'))
}
}
55 changes: 55 additions & 0 deletions src/main/groovy/se/solrike/cloudformation/StackUtils.groovy
Original file line number Diff line number Diff line change
@@ -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'.")
}
}
}

0 comments on commit d9f26d1

Please sign in to comment.