Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

Commit

Permalink
Redis bug bash (#49)
Browse files Browse the repository at this point in the history
* fix IDs, use cluster name
* simplify acornfile info, fix IDs with empty prefix
* add subnet group as replication group dependency
* add ID validation test
* add automatic snapshots with 1 day of retention
* add snapshot on delete with arg to disable, remove example gettingstarted.md
* move info and run formatter
  • Loading branch information
sosodev authored Sep 15, 2023
1 parent 9f67513 commit 1739505
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 66 deletions.
25 changes: 15 additions & 10 deletions elasticache/common.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package elasticache

import (
"crypto/md5"
"encoding/hex"
"os"
"strings"

"github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
"github.com/aws/aws-cdk-go/awscdk/v2/awselasticache"
Expand All @@ -11,15 +12,19 @@ import (
)

// ResourceID returns an ID that can be used to uniquely identify resources built with the given prefix
func ResourceID(prefix string) string {
id := os.Getenv("ACORN_EXTERNAL_ID")
id = strings.ReplaceAll(id, ".", "")

if len(id) > 40 {
return id[:40]
} else {
return id
func ResourceID(clusterName string, prefix string) *string {
externalIdHash := md5.Sum([]byte(os.Getenv("ACORN_EXTERNAL_ID")))
clusterName = clusterName + "-" + hex.EncodeToString(externalIdHash[:])

if prefix != "" {
clusterName = prefix + "-" + clusterName
}

if len(clusterName) > 40 {
clusterName = clusterName[:40]
}

return jsii.String(clusterName)
}

// GetPrivateSubnetGroup returns a new subnet group for the given elasticache stack
Expand All @@ -31,7 +36,7 @@ func GetPrivateSubnetGroup(scope constructs.Construct, name *string, vpc awsec2.
}

subnetGroup := awselasticache.NewCfnSubnetGroup(scope, name, &awselasticache.CfnSubnetGroupProps{
CacheSubnetGroupName: jsii.String(ResourceID("Sg")),
CacheSubnetGroupName: name,
Description: jsii.String("Acorn created Elasticache subnet group."),
SubnetIds: &privateSubnetIDs,
})
Expand Down
23 changes: 23 additions & 0 deletions elasticache/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package elasticache

import (
"os"
"testing"
)

func TestResourceID(t *testing.T) {
err := os.Setenv("ACORN_EXTERNAL_ID", "totally-real-and-cool-external-id-123")
if err != nil {
t.Fatal(err)
}

id := ResourceID("Redis", "Sng")
if len(*id) == 0 || len(*id) > 40 {
t.Errorf("invalid ID %s", *id)
}

idAgain := ResourceID("Redis", "Sng")
if *id != *idAgain {
t.Error("expected matching IDs")
}
}
47 changes: 26 additions & 21 deletions elasticache/redis/Acornfile
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
name: "AWS Elasticache Redis"
description: "Amazon's Redis compatible Elasticache cluster"
info: "\(localData.info)"
readme: "./README.md"

localData: info: """
## How To Use ([examples](https://github.com/acorn-io/aws/tree/main/elasticache/redis/examples))

1) Link your app with this acorn via an `external` service named "redis".

```typescript
services: redis: {
external: "@{acorn.name}"
}
containers: app: {
build: context: "./"
ports: publish: ["8080/http"]
env: {
REDIS_URL: "redis://:@{@{service.}redis.secrets.admin.token}@{@{service.}redis.address}:@{@{service.}redis.port}/0"
}
}
```
"""
readme: "./README.md"

args: {
// Name assigned to the cluster during creation. Default value is "RedisCluster".
// Name assigned to the cluster during creation alongside a unique ID. Default value is "Redis".
clusterName: "Redis"

// Key value pairs to apply to all resources.
Expand All @@ -37,6 +18,9 @@ args: {

// The number of cache nodes used in the elasticache cluster. Default value is 1. Automatic failover is enabled for values >1. Cluster mode is disabled so its a single primary with read replicas.
numNodes: 1

// Do not take a final snapshot on delete or update and replace operations. Default value is false. If skip is enabled your data since last snapshot will be gone forever if deleted or replaced.
skipSnapshotOnDelete: false
}

services: admin: {
Expand Down Expand Up @@ -113,3 +97,24 @@ secrets: "aws-context": {
"aws-region": ""
}
}

localData: info: """
## How To Use ([examples](https://github.com/acorn-io/aws/tree/main/elasticache/redis/examples))

1) Link your app with this acorn via an `external` service named "redis".

```typescript
services: redis: {
external: "@{acorn.name}"
}
containers: app: {
build: context: "./"
ports: publish: ["8080/http"]
env: {
REDIS_HOST: "@{@{service.}redis.address}"
REDIS_PORT: "@{@{service.}redis.data.port}"
REDIS_PASSWORD: "@{@{service.}redis.secrets.admin.token}"
}
}
```
"""
28 changes: 11 additions & 17 deletions elasticache/redis/examples/echo-app/Acornfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
services: redis: build: {
context: "../../"
context: "../../"
acornfile: "../../Acornfile"
}

containers: {
app: {
build: {
context: "."
}
ports: publish: ["5000/http"]
if args.dev {
dirs: {
"/src": "./"
}
}
env: {
REDIS_HOST: "@{service.redis.address}"
REDIS_PASSWORD: "@{service.redis.secrets.admin.token}"
}
}
containers: app: {
build: context: "."
ports: publish: ["5000/http"]
if args.dev {
dirs: "/src": "./"
}
env: {
REDIS_HOST: "@{service.redis.address}"
REDIS_PASSWORD: "@{service.redis.secrets.admin.token}"
}
}
11 changes: 0 additions & 11 deletions elasticache/redis/examples/echo-app/GettingStarted.md

This file was deleted.

27 changes: 20 additions & 7 deletions elasticache/redis/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (

type redisStackProps struct {
awscdk.StackProps
ClusterName string `json:"clusterName" yaml:"clusterName"`
UserTags map[string]string `json:"tags" yaml:"tags"`
NodeType string `json:"nodeType" yaml:"nodeType"`
NumNodes int `json:"numNodes" yaml:"numNodes"`
ClusterName string `json:"clusterName" yaml:"clusterName"`
UserTags map[string]string `json:"tags" yaml:"tags"`
NodeType string `json:"nodeType" yaml:"nodeType"`
NumNodes int `json:"numNodes" yaml:"numNodes"`
SkipSnapshotOnDelete bool `json:"skipSnapshotOnDelete" yaml:"skipSnapshotOnDelete"`
}

// NewRedisStack creates the new Redis stack
Expand All @@ -36,10 +37,10 @@ func NewRedisStack(scope constructs.Construct, id string, props *redisStackProps
})

// get the subnet group
subnetGroup := elasticache.GetPrivateSubnetGroup(stack, jsii.String(props.ClusterName+"SubnetGroup"), vpc)
subnetGroup := elasticache.GetPrivateSubnetGroup(stack, elasticache.ResourceID(props.ClusterName, "Sng"), vpc)

// get the security group
sg := common.GetAllowAllVPCSecurityGroup(stack, jsii.String(props.ClusterName+"SecurityGroup"), jsii.String("Acorn generated Elasticache security group"), vpc, 6379)
sg := common.GetAllowAllVPCSecurityGroup(stack, elasticache.ResourceID(props.ClusterName, "Scg"), jsii.String("Acorn generated Elasticache security group"), vpc, 6379)

vpcSecurityGroupIDs := make([]*string, 0)
vpcSecurityGroupIDs = append(vpcSecurityGroupIDs, sg.SecurityGroupId())
Expand All @@ -58,7 +59,7 @@ func NewRedisStack(scope constructs.Construct, id string, props *redisStackProps
// it might seem like creating a replication group is not the same as creating a cluster
// but actually it creates the cluster and the replication group in one go
redisRG := awselasticache.NewCfnReplicationGroup(stack, jsii.String(props.ClusterName), &awselasticache.CfnReplicationGroupProps{
ReplicationGroupId: jsii.String(elasticache.ResourceID("Rg")),
ReplicationGroupId: elasticache.ResourceID(props.ClusterName, ""),
ReplicationGroupDescription: jsii.String("Acorn created Redis replication group"),
Engine: jsii.String("redis"),
CacheNodeType: jsii.String(props.NodeType),
Expand All @@ -71,8 +72,20 @@ func NewRedisStack(scope constructs.Construct, id string, props *redisStackProps
SecurityGroupIds: &vpcSecurityGroupIDs,
AuthToken: token.SecretValue().ToString(),
Port: jsii.Number(6379),
SnapshotRetentionLimit: jsii.Number(1), // how many days to retain snapshots
})

// indicate that the subnet group depends on the cluster
// this prevents deletion errors caused by attempted subnet group deletes while the cluster still exists
redisRG.AddDependency(subnetGroup)

if !props.SkipSnapshotOnDelete {
// indicate that the cluster should be backed up before deletion
redisRG.ApplyRemovalPolicy(awscdk.RemovalPolicy_SNAPSHOT, &awscdk.RemovalPolicyOptions{
ApplyToUpdateReplacePolicy: jsii.Bool(true),
})
}

// output the cluster details
awscdk.NewCfnOutput(stack, jsii.String("clustername"), &awscdk.CfnOutputProps{
Value: jsii.String(props.ClusterName),
Expand Down

0 comments on commit 1739505

Please sign in to comment.