diff --git a/rds/aurora/mysql/cluster/Acornfile b/rds/aurora/mysql/cluster/Acornfile index afc2bab..45941a7 100644 --- a/rds/aurora/mysql/cluster/Acornfile +++ b/rds/aurora/mysql/cluster/Acornfile @@ -25,6 +25,8 @@ args: { instanceSize: "medium" // RDS MySQL Database Parameters to apply to the cluster. Must be k/v string pairs(ex. max_connections: "1000"). parameters: {} + // Creates a new cluster from this snapshot or revert the existing database cluster to this snapshot. Once this has been set, should remain the same on subsequent runs. Default is "". + restoreFromSnapshotArn: "" // Do not take a final snapshot on delete or update and replace operations. Default is false. If skip is enabled the DB will be gone forever if deleted or replaced. skipSnapshotOnDelete: false // Enable Performance insights. Default is false. @@ -39,15 +41,7 @@ services: rds: { } jobs: apply: { - build: { - context: "../../../" - dockerfile: "../../../mysql.Dockerfile" - buildArgs: MAIN: "cluster" - additionalContexts: { - common: "../../../../libs" - utils: "../../../../utils" - } - } + build: images.cdk.containerBuild files: "/app/config.json": std.toJSON(args) memory: 512Mi env: { @@ -126,6 +120,13 @@ images: user: containerBuild: { target: "user" } +images: cdk: containerBuild: { + context: "../../../" + dockerfile: "../../../mysql.Dockerfile" + buildArgs: MAIN: "cluster" + additionalContexts: common: "../../../../libs" +} + secrets: admin: { type: "generated" params: job: "apply" diff --git a/rds/aurora/mysql/cluster/rds.go b/rds/aurora/mysql/cluster/rds.go index 9693393..ccd023b 100644 --- a/rds/aurora/mysql/cluster/rds.go +++ b/rds/aurora/mysql/cluster/rds.go @@ -69,6 +69,10 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St }), }) + if props.RestoreSnapshotArn != "" { + awscdk.Aspects_Of(cluster).Add(rds.NewSnapshotAspect(props.RestoreSnapshotArn)) + } + port := "3306" pSlice := strings.SplitN(*cluster.ClusterEndpoint().SocketAddress(), ":", 2) if len(pSlice) == 2 { @@ -87,6 +91,9 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St awscdk.NewCfnOutput(stack, jsii.String("adminpasswordarn"), &awscdk.CfnOutputProps{ Value: cluster.Secret().SecretArn(), }) + awscdk.NewCfnOutput(stack, jsii.String("clusterid"), &awscdk.CfnOutputProps{ + Value: cluster.ClusterIdentifier(), + }) return stack } diff --git a/rds/aurora/mysql/serverless-v1/Acornfile b/rds/aurora/mysql/serverless-v1/Acornfile index 2572ae2..6a5f4eb 100644 --- a/rds/aurora/mysql/serverless-v1/Acornfile +++ b/rds/aurora/mysql/serverless-v1/Acornfile @@ -22,6 +22,8 @@ args: { auroraCapacityUnitsMax: 8 // Time in minutes to pause Aurora serverless-v1 DB cluster after it's been idle. Default is 10 set to 0 to disable. autoPauseDurationMinutes: 10 + // Create a new cluster from this snapshot or revert the existing database cluster to this snapshot. Once this has been set, should remain the same on subsequent runs. Default is "". + restoreFromSnapshotArn: "" // Do not take a final snapshot on delete or update and replace operations. Default is false. If skip is enabled the DB will be gone forever if deleted or replaced. skipSnapshotOnDelete: false // Key value pairs of tags to apply to the RDS cluster and all other resources. diff --git a/rds/aurora/mysql/serverless-v1/rds.go b/rds/aurora/mysql/serverless-v1/rds.go index b8cfd09..3f401a0 100644 --- a/rds/aurora/mysql/serverless-v1/rds.go +++ b/rds/aurora/mysql/serverless-v1/rds.go @@ -58,6 +58,10 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St ParameterGroup: parameterGroup, }) + if props.RestoreSnapshotArn != "" { + awscdk.Aspects_Of(cluster).Add(rds.NewSnapshotAspect(props.RestoreSnapshotArn)) + } + port := "3306" pSlice := strings.SplitN(*cluster.ClusterEndpoint().SocketAddress(), ":", 2) if len(pSlice) == 2 { @@ -76,6 +80,9 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St awscdk.NewCfnOutput(stack, jsii.String("adminpasswordarn"), &awscdk.CfnOutputProps{ Value: cluster.Secret().SecretArn(), }) + awscdk.NewCfnOutput(stack, jsii.String("clusterid"), &awscdk.CfnOutputProps{ + Value: cluster.ClusterIdentifier(), + }) return stack } diff --git a/rds/aurora/mysql/serverless-v2/Acornfile b/rds/aurora/mysql/serverless-v2/Acornfile index 5db1fd2..3b767e7 100644 --- a/rds/aurora/mysql/serverless-v2/Acornfile +++ b/rds/aurora/mysql/serverless-v2/Acornfile @@ -20,6 +20,8 @@ args: { auroraCapacityUnitsV2Max: *8.0 | float | int // RDS MySQL Database Parameters to apply to the cluster. Must be k/v string pairs(ex. max_connections: "1000"). parameters: {} + // Creates a new cluster from this snapshot or revert the existing database cluster to this snapshot. Once this has been set, should remain the same on subsequent runs. Default is "". + restoreFromSnapshotArn: "" // Do not take a final snapshot on delete or update and replace operations. Default is false. If skip is enabled the DB will be gone forever if deleted or replaced. skipSnapshotOnDelete: false // Enable Performance Insights. Default is false. diff --git a/rds/aurora/mysql/serverless-v2/rds.go b/rds/aurora/mysql/serverless-v2/rds.go index 3dc261c..b015148 100644 --- a/rds/aurora/mysql/serverless-v2/rds.go +++ b/rds/aurora/mysql/serverless-v2/rds.go @@ -60,6 +60,10 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St ParameterGroup: parameterGroup, }) + if props.RestoreSnapshotArn != "" { + awscdk.Aspects_Of(cluster).Add(rds.NewSnapshotAspect(props.RestoreSnapshotArn)) + } + port := "3306" pSlice := strings.SplitN(*cluster.ClusterEndpoint().SocketAddress(), ":", 2) if len(pSlice) == 2 { @@ -78,6 +82,9 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St awscdk.NewCfnOutput(stack, jsii.String("adminpasswordarn"), &awscdk.CfnOutputProps{ Value: cluster.Secret().SecretArn(), }) + awscdk.NewCfnOutput(stack, jsii.String("clusterid"), &awscdk.CfnOutputProps{ + Value: cluster.ClusterIdentifier(), + }) return stack } diff --git a/rds/aurora/postgres/cluster/Acornfile b/rds/aurora/postgres/cluster/Acornfile index b50d42e..a733f6b 100644 --- a/rds/aurora/postgres/cluster/Acornfile +++ b/rds/aurora/postgres/cluster/Acornfile @@ -25,6 +25,8 @@ args: { instanceSize: "medium" // RDS PostgreSQL Database Parameters to apply to the cluster. Must be k/v string pairs(ex. max_connections: "1000"). parameters: {} + // Create a new cluster from this snapshot or revert the existing database cluster to this snapshot. Once this has been set, should remain the same on subsequent runs. Default is "". + restoreFromSnapshotArn: "" // Do not take a final snapshot on delete or update and replace operations. Default is false. If skip is enabled the DB will be gone forever if deleted or replaced. skipSnapshotOnDelete: false // Enable Performance insights. Default is false. diff --git a/rds/aurora/postgres/cluster/rds.go b/rds/aurora/postgres/cluster/rds.go index 631338d..ba02681 100644 --- a/rds/aurora/postgres/cluster/rds.go +++ b/rds/aurora/postgres/cluster/rds.go @@ -64,6 +64,10 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St }), }) + if props.RestoreSnapshotArn != "" { + awscdk.Aspects_Of(cluster).Add(rds.NewSnapshotAspect(props.RestoreSnapshotArn)) + } + port := "5432" pSlice := strings.SplitN(*cluster.ClusterEndpoint().SocketAddress(), ":", 2) if len(pSlice) == 2 { @@ -82,6 +86,9 @@ func NewRDSStack(scope constructs.Construct, props *rds.RDSStackProps) awscdk.St awscdk.NewCfnOutput(stack, jsii.String("adminpasswordarn"), &awscdk.CfnOutputProps{ Value: cluster.Secret().SecretArn(), }) + awscdk.NewCfnOutput(stack, jsii.String("clusterid"), &awscdk.CfnOutputProps{ + Value: cluster.ClusterIdentifier(), + }) return stack } diff --git a/rds/common.go b/rds/common.go index 2635dd3..2e41150 100644 --- a/rds/common.go +++ b/rds/common.go @@ -35,7 +35,7 @@ type RDSStackProps struct { InstanceClass string `json:"instanceClass"` InstanceSize string `json:"instanceSize"` Parameters map[string]string `json:"parameters"` - RestoreSnapshotArn string `json:"restoreSnapshotArn"` + RestoreSnapshotArn string `json:"restoreFromSnapshotArn"` SkipSnapShotOnDelete bool `json:"skipSnapshotOnDelete"` Tags map[string]string `json:"tags"` VpcID string @@ -48,6 +48,22 @@ type RDSStackProps struct { AuroraCapacityUnitsV2Max float64 `json:"auroraCapacityUnitsV2Max"` } +type SnapshotAspect struct { + SnapshotIdentifier string +} + +func (sa *SnapshotAspect) Visit(node constructs.IConstruct) { + if n, ok := node.(awsrds.CfnDBCluster); ok { + n.AddPropertyOverride(jsii.String("SnapshotIdentifier"), jsii.String(sa.SnapshotIdentifier)) + } +} + +func NewSnapshotAspect(snapshotIdentifier string) *SnapshotAspect { + return &SnapshotAspect{ + SnapshotIdentifier: snapshotIdentifier, + } +} + func NewParameterGroup(scope constructs.Construct, name *string, props *RDSStackProps, engine awsrds.IClusterEngine) awsrds.ParameterGroup { parameterGroup := awsrds.NewParameterGroup(scope, name, &awsrds.ParameterGroupProps{ Engine: engine, diff --git a/rds/hooks/pre-change-set-apply b/rds/hooks/pre-change-set-apply new file mode 100755 index 0000000..6dcc782 --- /dev/null +++ b/rds/hooks/pre-change-set-apply @@ -0,0 +1,41 @@ +#!/bin/bash + +# On delete, do not get involved, the user might be trying to disable delete protection. +if [ "${ACORN_EVENT}" = "delete" ]; then + echo "Skipping pre-apply hook on delete event." + exit 0 +fi + +write_error() { + echo "Error: $1" >&2 + exit 1 +} + +help() { + echo "Usage: $0 " + write_error "Usage: $0 " >&2 +} + +snapshot_identifier_present() { + grep "SnapshotIdentifier" "${1}" > /dev/null + return $? +} + +if [ "$#" -ne 3 ]; then + help +fi + +current_cfn_template="${1}" +proposed_cfn_template="${2}" +change_set="${3}" + + +snapshot_identifier_present "${current_cfn_template}" +current_snapshot=$? +snapshot_identifier_present "${proposed_cfn_template}" +proposed_snapshot=$? + +if [ "${current_snapshot}" -eq 0 ] && [ "${proposed_snapshot}" -eq 1 ]; then + value=$(grep "SnapshotIdentifier" "${current_cfn_template}" | awk '{print $2}') + write_error "Cannot change from snapshot ${value} to no snapshot. You must delete Acorn ${ACORN_NAME} to reset." +fi \ No newline at end of file diff --git a/rds/mysql.Dockerfile b/rds/mysql.Dockerfile index 777332f..9f0dd46 100644 --- a/rds/mysql.Dockerfile +++ b/rds/mysql.Dockerfile @@ -5,14 +5,14 @@ COPY --from=common . ../libs/ COPY . . RUN --mount=type=cache,target=/root/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - go build -o rds ./aurora/mysql/${MAIN} + go build -o rds ./aurora/mysql/${MAIN} FROM cgr.dev/chainguard/mariadb as user WORKDIR /app COPY ./scripts ./scripts ENTRYPOINT ["/app/scripts/create_and_grant_users.sh"] -FROM ghcr.io/acorn-io/aws/utils/cdk-runner:v0.6.0 as cdk-runner +FROM ghcr.io/acorn-io/aws/utils/cdk-runner:v0.7.1 as cdk-runner FROM cgr.dev/chainguard/wolfi-base RUN apk add -U --no-cache nodejs bash busybox jq curl zip && \ apk del --no-cache wolfi-base apk-tools @@ -23,6 +23,7 @@ RUN npm install -g aws-cdk WORKDIR /app COPY ./cdk.json ./ COPY ./scripts ./scripts +COPY ./hooks ./hooks COPY --from=cdk-runner /cdk-runner . COPY --from=build /src/rds/rds . diff --git a/rds/postgres.Dockerfile b/rds/postgres.Dockerfile index 2d9b0f4..9c8b12a 100644 --- a/rds/postgres.Dockerfile +++ b/rds/postgres.Dockerfile @@ -13,17 +13,18 @@ WORKDIR /app COPY ./scripts ./scripts ENTRYPOINT ["/app/scripts/create_and_grant_users_psql.sh"] -FROM ghcr.io/acorn-io/aws/utils/cdk-runner:v0.6.0 as cdk-runner +FROM ghcr.io/acorn-io/aws/utils/cdk-runner:v0.7.1 as cdk-runner FROM cgr.dev/chainguard/wolfi-base RUN apk add -U --no-cache nodejs bash busybox jq curl zip && \ apk del --no-cache wolfi-base apk-tools RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ - unzip awscliv2.zip && \ - ./aws/install + unzip awscliv2.zip && \ + ./aws/install RUN npm install -g aws-cdk WORKDIR /app COPY ./cdk.json ./ COPY ./scripts ./scripts +COPY ./hooks ./hooks COPY --from=cdk-runner /cdk-runner . COPY --from=build /src/rds/rds . diff --git a/rds/scripts/list-snapshots.sh b/rds/scripts/list-snapshots.sh new file mode 100755 index 0000000..aaba425 --- /dev/null +++ b/rds/scripts/list-snapshots.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -x +aws rds describe-db-cluster-snapshots --db-cluster-identifier "${DB_CLUSTER_ID}" --query "DBClusterSnapshots[*].DBClusterSnapshotArn" | jq -r '.[]' > /run/secrets/output \ No newline at end of file diff --git a/rds/scripts/service.sh b/rds/scripts/service.sh index 0564aaf..1decfac 100755 --- a/rds/scripts/service.sh +++ b/rds/scripts/service.sh @@ -5,6 +5,7 @@ PORT="$( jq -r '.[] | select(.OutputKey=="port") |.OutputVal ADDRESS="$( jq -r '.[] | select(.OutputKey=="host") |.OutputValue' outputs.json )" ADMIN_USERNAME="$(jq -r '.[] | select(.OutputKey=="adminusername") |.OutputValue' outputs.json )" PASSWORD_ARN="$( jq -r '.[] | select(.OutputKey=="adminpasswordarn")|.OutputValue' outputs.json )" +CLUSTER_ID="$( jq -r '.[] | select(.OutputKey=="clusterid") |.OutputValue' outputs.json )" ADMIN_PASSWORD="$(aws --output json secretsmanager get-secret-value --secret-id "${PASSWORD_ARN}" --query 'SecretString' | jq -r .|jq -r .password)" @@ -15,6 +16,7 @@ services: rds: { ports: [${PORT}] data: { dbName: "${DB_NAME}" + clusterId: "${CLUSTER_ID}" } }