Skip to content

Commit 3d9eaf1

Browse files
committed
Migrate 016-opensearch and 043-mq to CloudFormation for resource creation
Replace CLI create + wait loops with cloudformation deploy. CFN handles the 15-20 min creation wait, rollback on failure, and guaranteed cleanup via delete-stack. Eliminates timeout-induced resource leaks. 016-opensearch: cfn-opensearch-domain.yaml (domain + access policy + encryption) 043-amazon-mq: cfn-mq-broker.yaml (broker + user + public access) Scripts still use CLI for tutorial steps (indexing, messaging). Only the long-running infrastructure creation moves to CFN.
1 parent 05defbb commit 3d9eaf1

4 files changed

Lines changed: 187 additions & 156 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Description: OpenSearch domain for the Getting Started tutorial
3+
4+
Parameters:
5+
DomainName:
6+
Type: String
7+
Description: Name for the OpenSearch domain
8+
MasterUserName:
9+
Type: String
10+
Default: admin
11+
MasterUserPassword:
12+
Type: String
13+
NoEcho: true
14+
15+
Resources:
16+
OpenSearchDomain:
17+
Type: AWS::OpenSearchService::Domain
18+
Properties:
19+
DomainName: !Ref DomainName
20+
EngineVersion: OpenSearch_2.11
21+
ClusterConfig:
22+
InstanceType: t3.small.search
23+
InstanceCount: 1
24+
ZoneAwarenessEnabled: false
25+
EBSOptions:
26+
EBSEnabled: true
27+
VolumeType: gp3
28+
VolumeSize: 10
29+
NodeToNodeEncryptionOptions:
30+
Enabled: true
31+
EncryptionAtRestOptions:
32+
Enabled: true
33+
DomainEndpointOptions:
34+
EnforceHTTPS: true
35+
TLSSecurityPolicy: Policy-Min-TLS-1-2-2019-07
36+
AdvancedSecurityOptions:
37+
Enabled: true
38+
InternalUserDatabaseEnabled: true
39+
MasterUserOptions:
40+
MasterUserName: !Ref MasterUserName
41+
MasterUserPassword: !Ref MasterUserPassword
42+
AccessPolicies:
43+
Version: '2012-10-17'
44+
Statement:
45+
- Effect: Allow
46+
Principal:
47+
AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
48+
Action:
49+
- 'es:ESHttpGet'
50+
- 'es:ESHttpPut'
51+
- 'es:ESHttpPost'
52+
- 'es:ESHttpDelete'
53+
- 'es:ESHttpHead'
54+
Resource: !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${DomainName}/*'
55+
Tags:
56+
- Key: project
57+
Value: doc-smith
58+
- Key: tutorial
59+
Value: opensearch-service-gs
60+
61+
Outputs:
62+
DomainEndpoint:
63+
Value: !GetAtt OpenSearchDomain.DomainEndpoint
64+
DomainArn:
65+
Value: !GetAtt OpenSearchDomain.Arn

tuts/016-opensearch-service-gs/opensearch-service-gs.sh

Lines changed: 41 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -112,101 +112,40 @@ echo "This may take 15-30 minutes to complete."
112112

113113
# SECURITY IMPROVED: Use least privilege access policy
114114
# This policy restricts access to specific actions and should be further restricted to specific principals in production
115-
ACCESS_POLICY="{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::${ACCOUNT_ID}:root\"},\"Action\":[\"es:ESHttpGet\",\"es:ESHttpPut\",\"es:ESHttpPost\",\"es:ESHttpDelete\",\"es:ESHttpHead\"],\"Resource\":\"arn:aws:es:${AWS_REGION}:${ACCOUNT_ID}:domain/${DOMAIN_NAME}/*\"}]}"
116-
117-
echo "Access policy created for region: $AWS_REGION"
118-
echo "Access policy: [REDACTED]"
119-
120-
# Create the domain (matches tutorial command exactly)
121-
echo "Creating domain $DOMAIN_NAME..."
122-
CREATE_OUTPUT=$(aws opensearch create-domain \
123-
--domain-name "$DOMAIN_NAME" \
124-
--engine-version "OpenSearch_2.11" \
125-
--cluster-config "InstanceType=t3.small.search,InstanceCount=1,ZoneAwarenessEnabled=false" \
126-
--ebs-options "EBSEnabled=true,VolumeType=gp3,VolumeSize=10" \
127-
--node-to-node-encryption-options "Enabled=true" \
128-
--encryption-at-rest-options "Enabled=true" \
129-
--domain-endpoint-options "EnforceHTTPS=true,TLSSecurityPolicy=Policy-Min-TLS-1-2-2019-07" \
130-
--advanced-security-options "Enabled=true,InternalUserDatabaseEnabled=true,MasterUserOptions={MasterUserName=$MASTER_USER,MasterUserPassword=$MASTER_PASSWORD}" \
131-
--access-policies "$ACCESS_POLICY" \
132-
--tags "Key=Environment,Value=Tutorial" "Key=Purpose,Value=OpenSearchGettingStarted" "Key=project,Value=doc-smith" "Key=tutorial,Value=opensearch-service-gs" 2>&1)
133-
134-
# Check if domain creation was successful
135-
if [[ $? -ne 0 ]]; then
136-
echo "Failed to create OpenSearch domain:"
137-
echo "$CREATE_OUTPUT"
138-
handle_error "Domain creation failed"
139-
fi
140-
141-
# Verify the domain was actually created by checking the output
142-
if echo "$CREATE_OUTPUT" | grep -q "DomainStatus"; then
143-
echo "Domain creation initiated successfully."
144-
DOMAIN_CREATED=true
145-
else
146-
echo "Domain creation output:"
147-
echo "$CREATE_OUTPUT"
148-
handle_error "Domain creation may have failed - no DomainStatus in response"
149-
fi
150-
151-
# Wait for domain to become active (improved logic)
152-
echo "Waiting for domain to become active..."
153-
RETRY_COUNT=0
154-
MAX_RETRIES=45 # 45 minutes with 60 second intervals
155-
156-
while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
157-
echo "Checking domain status... (attempt $((RETRY_COUNT+1))/$MAX_RETRIES)"
158-
159-
# Get domain status
160-
DOMAIN_STATUS=$(aws opensearch describe-domain --domain-name "$DOMAIN_NAME" 2>&1)
161-
162-
if [[ $? -ne 0 ]]; then
163-
echo "Error checking domain status:"
164-
echo "$DOMAIN_STATUS"
165-
166-
# If domain not found after several attempts, it likely failed to create
167-
if [[ $RETRY_COUNT -gt 5 ]] && echo "$DOMAIN_STATUS" | grep -q "ResourceNotFoundException"; then
168-
handle_error "Domain not found after multiple attempts. Domain creation likely failed."
169-
fi
170-
171-
echo "Will retry in 60 seconds..."
172-
else
173-
# Check if domain is no longer processing
174-
if echo "$DOMAIN_STATUS" | grep -q '"Processing": false'; then
175-
DOMAIN_ACTIVE=true
176-
echo "Domain is now active!"
177-
break
178-
else
179-
echo "Domain is still being created. Checking again in 60 seconds..."
180-
fi
181-
fi
182-
183-
sleep 60
184-
RETRY_COUNT=$((RETRY_COUNT+1))
185-
done
186-
187-
# Verify domain is active
188-
if [[ "$DOMAIN_ACTIVE" != "true" ]]; then
189-
echo "Domain creation is taking longer than expected ($((MAX_RETRIES)) minutes)."
190-
echo "You can check the status later using:"
191-
echo "aws opensearch describe-domain --domain-name $DOMAIN_NAME"
192-
handle_error "Domain did not become active within the expected time"
193-
fi
194-
195-
# Get domain endpoint (matches tutorial)
196-
echo "Retrieving domain endpoint..."
197-
DOMAIN_ENDPOINT=$(aws opensearch describe-domain --domain-name "$DOMAIN_NAME" --query 'DomainStatus.Endpoint' --output text)
198-
199-
if [[ $? -ne 0 ]] || [[ -z "$DOMAIN_ENDPOINT" ]] || [[ "$DOMAIN_ENDPOINT" == "None" ]]; then
200-
handle_error "Failed to get domain endpoint"
115+
# Create the domain using CloudFormation (handles the 15-20 min creation wait)
116+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
117+
STACK_NAME="tutorial-opensearch-${RANDOM_ID}"
118+
119+
echo "Creating domain $DOMAIN_NAME via CloudFormation..."
120+
echo "This typically takes 15-20 minutes. CloudFormation will wait for completion."
121+
aws cloudformation deploy \
122+
--template-file "$SCRIPT_DIR/cfn-opensearch-domain.yaml" \
123+
--stack-name "$STACK_NAME" \
124+
--parameter-overrides \
125+
DomainName="$DOMAIN_NAME" \
126+
MasterUserName="$MASTER_USER" \
127+
MasterUserPassword="$MASTER_PASSWORD" \
128+
--tags project=doc-smith tutorial=opensearch-service-gs \
129+
--no-fail-on-empty-changeset 2>&1 || handle_error "CloudFormation stack creation failed"
130+
131+
DOMAIN_CREATED=true
132+
echo "Domain created successfully via CloudFormation."
133+
134+
# Get domain endpoint from stack outputs
135+
DOMAIN_ENDPOINT=$(aws cloudformation describe-stacks \
136+
--stack-name "$STACK_NAME" \
137+
--query 'Stacks[0].Outputs[?OutputKey==`DomainEndpoint`].OutputValue' \
138+
--output text)
139+
140+
if [[ -z "$DOMAIN_ENDPOINT" ]] || [[ "$DOMAIN_ENDPOINT" == "None" ]]; then
141+
handle_error "Failed to get domain endpoint from CloudFormation outputs"
201142
fi
202143

203144
echo "Domain endpoint: $DOMAIN_ENDPOINT"
204145

205-
# Wait additional time for fine-grained access control to be fully ready
206-
echo "Domain is active, but waiting additional time for fine-grained access control to be fully ready..."
207-
echo "Fine-grained access control can take several minutes to initialize after domain becomes active."
208-
echo "Waiting 8 minutes for full initialization..."
209-
sleep 480 # Wait 8 minutes for fine-grained access control to be ready
146+
# Wait for fine-grained access control to be fully ready
147+
echo "Waiting for fine-grained access control to initialize..."
148+
sleep 120
210149

211150
# Verify variables are set correctly (matches tutorial)
212151
echo "Verifying configuration..."
@@ -534,21 +473,22 @@ CLEANUP_CHOICE="y"
534473

535474
if [[ "${CLEANUP_CHOICE,,}" == "y" ]]; then
536475
echo "Cleaning up resources..."
537-
aws opensearch delete-domain --domain-name "$DOMAIN_NAME"
538-
echo "✓ Cleanup initiated. Domain deletion may take several minutes to complete."
539-
echo ""
540-
echo "You can check the deletion status using:"
541-
echo "aws opensearch describe-domain --domain-name $DOMAIN_NAME"
542-
echo ""
543-
echo "When deletion is complete, you'll see a 'Domain not found' error."
476+
if [ -n "${STACK_NAME:-}" ]; then
477+
echo "Deleting CloudFormation stack $STACK_NAME..."
478+
aws cloudformation delete-stack --stack-name "$STACK_NAME"
479+
echo "✓ Stack deletion initiated. This may take several minutes."
480+
echo " Monitor: aws cloudformation describe-stacks --stack-name $STACK_NAME"
481+
else
482+
aws opensearch delete-domain --domain-name "$DOMAIN_NAME" 2>/dev/null
483+
echo "✓ Domain deletion initiated."
484+
fi
544485
else
545486
echo "Resources will NOT be deleted automatically."
546487
echo ""
547-
echo "To delete the domain later, use:"
548-
echo "aws opensearch delete-domain --domain-name $DOMAIN_NAME"
488+
echo "To delete later:"
489+
echo " aws cloudformation delete-stack --stack-name ${STACK_NAME:-tutorial-opensearch-XXXX}"
549490
echo ""
550491
echo "⚠ IMPORTANT: Keeping these resources will incur ongoing AWS charges!"
551-
echo " Estimated cost: ~$0.038/hour (~$0.91/day)"
552492
fi
553493

554494
# Clean up temporary files (handled by trap)

tuts/043-amazon-mq-gs/amazon-mq-gs.sh

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ handle_error() {
5555
cleanup_resources() {
5656
echo "Cleaning up resources..."
5757

58-
if [ -n "${BROKER_ID:-}" ]; then
58+
# Prefer CloudFormation stack deletion (handles broker + dependencies)
59+
if [ -n "${STACK_NAME:-}" ]; then
60+
echo "Deleting CloudFormation stack: $STACK_NAME"
61+
aws cloudformation delete-stack --stack-name "$STACK_NAME" 2>/dev/null
62+
echo "Stack deletion initiated."
63+
elif [ -n "${BROKER_ID:-}" ]; then
5964
echo "Deleting Amazon MQ broker: $BROKER_ID"
6065
if ! aws mq delete-broker --broker-id "$BROKER_ID" 2>/dev/null; then
6166
echo "Warning: Failed to delete broker or broker already deleted"
@@ -132,64 +137,34 @@ echo "Creating Amazon MQ broker: $BROKER_NAME"
132137
echo "WARNING: Broker is being created with public accessibility for tutorial purposes only"
133138
echo "In production, use private subnets and proper network controls"
134139

135-
BROKER_RESULT=$(aws mq create-broker \
136-
--broker-name "$BROKER_NAME" \
137-
--engine-type ACTIVEMQ \
138-
--engine-version 5.18 \
139-
--host-instance-type mq.t3.micro \
140-
--deployment-mode SINGLE_INSTANCE \
141-
--authentication-strategy SIMPLE \
142-
--users "Username=$MQ_USERNAME,Password=$MQ_PASSWORD,ConsoleAccess=true" \
143-
--publicly-accessible \
144-
--auto-minor-version-upgrade \
145-
--storage-type EFS \
146-
--tags project=doc-smith,tutorial=amazon-mq-gs \
147-
2>&1)
148-
149-
# Check for errors
150-
if echo "$BROKER_RESULT" | grep -i "error" > /dev/null; then
151-
handle_error "Failed to create broker: $BROKER_RESULT"
152-
fi
140+
# Deploy via CloudFormation (handles the 15-20 min creation wait)
141+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
142+
STACK_NAME="tutorial-mq-${RANDOM_ID}"
143+
144+
echo "Deploying broker via CloudFormation. This typically takes 15-20 minutes..."
145+
aws cloudformation deploy \
146+
--template-file "$SCRIPT_DIR/cfn-mq-broker.yaml" \
147+
--stack-name "$STACK_NAME" \
148+
--parameter-overrides \
149+
BrokerName="$BROKER_NAME" \
150+
MQUsername="$MQ_USERNAME" \
151+
MQPassword="$MQ_PASSWORD" \
152+
--tags project=doc-smith tutorial=amazon-mq-gs \
153+
--no-fail-on-empty-changeset 2>&1 || handle_error "CloudFormation stack creation failed"
154+
155+
echo "Broker created successfully via CloudFormation."
156+
157+
# Extract broker ID from stack
158+
BROKER_ID=$(aws cloudformation describe-stacks \
159+
--stack-name "$STACK_NAME" \
160+
--query 'Stacks[0].Outputs[?OutputKey==`BrokerId`].OutputValue' \
161+
--output text)
153162

154-
# Extract broker ID using jq for safer parsing
155-
BROKER_ID=$(echo "$BROKER_RESULT" | jq -r '.BrokerId // empty')
156163
if [ -z "$BROKER_ID" ]; then
157-
handle_error "Failed to extract broker ID from response"
164+
handle_error "Failed to extract broker ID from CloudFormation outputs"
158165
fi
159166

160-
echo "Broker creation initiated. Broker ID: $BROKER_ID"
161-
162-
# Step 3: Wait for the broker to be in RUNNING state
163-
echo "Waiting for broker to be in RUNNING state. This may take 15-20 minutes..."
164-
MAX_ATTEMPTS=120
165-
ATTEMPT=0
166-
167-
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
168-
BROKER_STATE=$(aws mq describe-broker --broker-id "$BROKER_ID" --query 'BrokerState' --output text 2>&1)
169-
170-
if echo "$BROKER_STATE" | grep -i "error" > /dev/null; then
171-
handle_error "Error checking broker state: $BROKER_STATE"
172-
fi
173-
174-
echo "Current broker state: $BROKER_STATE (Attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)"
175-
176-
if [ "$BROKER_STATE" == "RUNNING" ]; then
177-
echo "Broker is now in RUNNING state"
178-
break
179-
elif [ "$BROKER_STATE" == "CREATION_FAILED" ]; then
180-
handle_error "Broker creation failed"
181-
fi
182-
183-
ATTEMPT=$((ATTEMPT + 1))
184-
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
185-
echo "Waiting 60 seconds before checking again..."
186-
sleep 60
187-
fi
188-
done
189-
190-
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
191-
handle_error "Broker did not reach RUNNING state within expected time"
192-
fi
167+
echo "Broker ID: $BROKER_ID"
193168

194169
# Step 4: Get broker connection details
195170
echo "Retrieving broker connection details..."
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Description: Amazon MQ broker for the Getting Started tutorial
3+
4+
Parameters:
5+
BrokerName:
6+
Type: String
7+
Description: Name for the MQ broker
8+
MQUsername:
9+
Type: String
10+
Default: tutorialAdmin
11+
MQPassword:
12+
Type: String
13+
NoEcho: true
14+
MinLength: 12
15+
16+
Resources:
17+
MQBroker:
18+
Type: AWS::AmazonMQ::Broker
19+
Properties:
20+
BrokerName: !Ref BrokerName
21+
EngineType: ACTIVEMQ
22+
EngineVersion: '5.18'
23+
HostInstanceType: mq.t3.micro
24+
DeploymentMode: SINGLE_INSTANCE
25+
AuthenticationStrategy: SIMPLE
26+
PubliclyAccessible: true
27+
AutoMinorVersionUpgrade: true
28+
StorageType: EFS
29+
Users:
30+
- Username: !Ref MQUsername
31+
Password: !Ref MQPassword
32+
ConsoleAccess: true
33+
Tags:
34+
- Key: project
35+
Value: doc-smith
36+
- Key: tutorial
37+
Value: amazon-mq-gs
38+
39+
Outputs:
40+
BrokerId:
41+
Value: !Ref MQBroker
42+
BrokerArn:
43+
Value: !GetAtt MQBroker.Arn
44+
AmqpEndpoint:
45+
Value: !Select [0, !GetAtt MQBroker.AmqpEndpoints]
46+
StompEndpoint:
47+
Value: !Select [0, !GetAtt MQBroker.StompEndpoints]
48+
OpenWireEndpoint:
49+
Value: !Select [0, !GetAtt MQBroker.OpenWireEndpoints]
50+
ConsoleURL:
51+
Value: !Sub 'https://${MQBroker}.mq.${AWS::Region}.amazonaws.com'

0 commit comments

Comments
 (0)