diff --git a/conf/.gitignore b/conf/.gitignore new file mode 100644 index 0000000..7bd20a6 --- /dev/null +++ b/conf/.gitignore @@ -0,0 +1,2 @@ +*.log +*.txt \ No newline at end of file diff --git a/conf/DBTopMonitoringSolution.yaml b/conf/DBTopMonitoringSolution.yaml new file mode 100644 index 0000000..504d5b3 --- /dev/null +++ b/conf/DBTopMonitoringSolution.yaml @@ -0,0 +1,426 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "DBTop Monitoring Solution - (uksb-fsqa5yre5y)." +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - + Label: + default: "Application Update - Disclaimer" + Parameters: + - ApplicationUpdateEnabled + - ApplicationUpdateUrl + + - + Label: + default: "General Configuration" + Parameters: + - Username + - AMIId + - InstanceType + - GitHubRepository + + - + Label: + default: "Network Configuration" + Parameters: + - VPCParam + - SubnetParam + - PublicAccess + - SGInboundAccess + + ParameterLabels: + Username: + default: "Specify username for application access, temporary credentials will be sent by email." + AMIId: + default: "Specify AWS Linux AMI to be used for Application Deployment." + InstanceType: + default: "Specify instance type for Application Deployment." + VPCParam: + default: "Select VPC for Application Deployment." + SubnetParam: + default: "Select Subnet for Application Deployment, this subnet needs internet outbound access to reach AWS APIs." + PublicAccess: + default: "The deployment will assign private IP Address by default to access the application, you can assign Public IP Address to access the application in case you need it, Select (true) to assign Public IP Address." + SGInboundAccess: + default: "Specify CIDR inbound access rule, this will grant network access for the application." + GitHubRepository: + default: "AWS Github Repository source used for deployment." + ApplicationUpdateEnabled: + default: "Disclaimer : This Application could able to verify new versions, it will help to keep update with most popular features. Customer can review the code and validate data scope. Selecting (true) I acknowledge that this Application will access to url to verify new versions." + ApplicationUpdateUrl: + default: "URL used to verify new application versions." + + +Parameters: + + VPCParam: + Type: AWS::EC2::VPC::Id + Description: Select VPC + + SubnetParam: + Type: AWS::EC2::Subnet::Id + Description: Select Subnet + + AMIId: + Type: AWS::SSM::Parameter::Value + Description: AWS AMI + Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' + + Username: + Type: String + Description: Username (email) + AllowedPattern: "\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}" + + InstanceType: + Type: String + Description: InstanceType + Default: t3a.medium + AllowedValues: + - t3a.micro + - t3a.small + - t3a.medium + - t3a.large + - t3a.xlarge + PublicAccess: + Type: String + Description: Assign Public IP Address + Default: "false" + AllowedValues: + - "true" + - "false" + + SGInboundAccess: + Type: String + Description: CIDR (0.0.0.0/0) + + GitHubRepository: + Type: String + Description: AWS Github Repository + Default: aws-samples + + ApplicationUpdateUrl: + Type: String + Description: URL + Default: https://version.code.ds.wwcs.aws.dev/ + + ApplicationUpdateEnabled: + Type: String + Description: Option + Default: "true" + AllowedValues: + - "true" + - "false" + +Conditions: + isPublic: !Equals [ !Ref PublicAccess, true] + +Resources: + InstanceProfile: + Type: "AWS::IAM::InstanceProfile" + DependsOn: IAMRoleEC2 + Properties: + Path: "/" + Roles: [!Ref IAMRoleEC2] + + + IAMPolicyEc2: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: !Join [ "-", ["policy-ec2-db-top-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:GetLogEvents" + ], + "Resource": [ + "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:RDSOSMetrics:log-stream:*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "cloudwatch:GetMetricData" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:DescribeDBInstances" + ], + "Resource": "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:db:*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:DescribeDBShardGroups" + ], + "Resource": "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:shard-group:*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:DescribeDBClusters" + ], + "Resource": "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:cluster:*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticache:DescribeReplicationGroups" + ], + "Resource": "arn:aws:elasticache:${AWS::Region}:${AWS::AccountId}:replicationgroup:*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticache:DescribeServerlessCaches" + ], + "Resource": "arn:aws:elasticache:${AWS::Region}:${AWS::AccountId}:serverlesscache:*" + }, + { + "Effect": "Allow", + "Action": "memorydb:DescribeClusters", + "Resource": "arn:aws:memorydb:${AWS::Region}:${AWS::AccountId}:cluster/*" + }, + { + "Effect": "Allow", + "Action": "docdb-elastic:GetCluster", + "Resource": "arn:aws:docdb-elastic:${AWS::Region}:${AWS::AccountId}:cluster/*" + }, + { + "Effect": "Allow", + "Action": "docdb-elastic:ListClusters", + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "dynamodb:DescribeTable", + "Resource": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*" + }, + { + "Effect": "Allow", + "Action": "dynamodb:ListTables", + "Resource": "*" + } + ] + } + + + IAMRoleEC2: + Type: "AWS::IAM::Role" + DependsOn: IAMPolicyEc2 + Properties: + Path: "/" + RoleName: !Join [ "-", ["role-ec2-db-top-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" + MaxSessionDuration: 3600 + Description: "Allows EC2 instance to call AWS services on your behalf." + ManagedPolicyArns: + - !Ref IAMPolicyEc2 + - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" + + IAMRoleCognito: + Type: "AWS::IAM::Role" + Properties: + Path: "/" + RoleName: !Join [ "-", ["role-cognito-db-top-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cognito-idp.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" + MaxSessionDuration: 3600 + Description: "Allows Cognito to use SMS MFA on your behalf." + Policies: + - PolicyName: "CognitoPolicy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "sns:publish" + Resource: "*" + + EC2Instance: + Type: "AWS::EC2::Instance" + DependsOn: [CognitoUserPool,IAMRoleEC2] + Properties: + ImageId: !Ref AMIId + InstanceType: !Ref InstanceType + Tenancy: "default" + EbsOptimized: true + SourceDestCheck: true + BlockDeviceMappings: + - + DeviceName: "/dev/xvda" + Ebs: + Encrypted: false + VolumeSize: 20 + VolumeType: "gp2" + DeleteOnTermination: true + IamInstanceProfile: !Ref InstanceProfile + NetworkInterfaces: + - AssociatePublicIpAddress: !Ref PublicAccess + DeviceIndex: "0" + GroupSet: + - Ref: VPCSecurityGroup + SubnetId: + Ref: SubnetParam + UserData: + Fn::Base64: + !Sub | + #!/bin/bash + sudo mkdir -p /aws/apps + + cd /tmp + sudo yum install -y git + git clone https://github.com/${GitHubRepository}/db-top-monitoring.git + cd db-top-monitoring + sudo cp -r server frontend conf /aws/apps + + echo '{ "aws_region": "${AWS::Region}","aws_cognito_user_pool_id": "${CognitoUserPool}","aws_cognito_user_pool_web_client_id": "${CognitoUserPoolClient}","aws_api_port": 3000, "aws_token_expiration":24, "aws_application_update_url" : "${ApplicationUpdateUrl}", "aws_application_update_enabled" : "${ApplicationUpdateEnabled}" }' > /aws/apps/conf/aws-exports.json + cd /aws/apps + sudo -u ec2-user sh conf/setup.sh 2>&1 | tee /tmp/setup.log + + sudo sed -i 's/GitHubRepository/${GitHubRepository}/g' /aws/apps/conf/update.sh + + Tags: + - + Key: "Name" + Value: !Join [ "-", ["ec2-db-top-solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + + + VPCSecurityGroup: + Type: "AWS::EC2::SecurityGroup" + Properties: + GroupDescription: !Join [ "_", ["sg_security_group_db_top_solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + GroupName: !Join [ "_", ["sg_security_group_db_top_solution", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + VpcId: !Ref VPCParam + SecurityGroupIngress: + - + CidrIp: !Ref SGInboundAccess + FromPort: 443 + IpProtocol: "tcp" + ToPort: 443 + + SecurityGroupEgress: + - + CidrIp: "0.0.0.0/0" + IpProtocol: "-1" + + + CognitoUserPool: + Type: "AWS::Cognito::UserPool" + Properties: + UserPoolName: !Join [ "-", ["AwsDbTopSolutionUserPool", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + Policies: + PasswordPolicy: + MinimumLength: 8 + RequireUppercase: true + RequireLowercase: true + RequireNumbers: true + RequireSymbols: true + TemporaryPasswordValidityDays: 7 + LambdaConfig: {} + AutoVerifiedAttributes: + - "email" + UsernameAttributes: + - "email" + MfaConfiguration: "OPTIONAL" + SmsConfiguration: + SnsCallerArn: !GetAtt IAMRoleCognito.Arn + SnsRegion: !Ref AWS::Region + EmailConfiguration: + EmailSendingAccount: "COGNITO_DEFAULT" + AdminCreateUserConfig: + AllowAdminCreateUserOnly: true + UserPoolTags: {} + AccountRecoverySetting: + RecoveryMechanisms: + - + Priority: 1 + Name: "verified_email" + UsernameConfiguration: + CaseSensitive: false + VerificationMessageTemplate: + DefaultEmailOption: "CONFIRM_WITH_CODE" + + CognitoUserPoolClient: + Type: "AWS::Cognito::UserPoolClient" + Properties: + UserPoolId: !Ref CognitoUserPool + ClientName: !Join [ "-", ["AwsDbTopSolutionUserPoolClient", !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + RefreshTokenValidity: 1 + ReadAttributes: + - "address" + - "birthdate" + - "email" + - "email_verified" + - "family_name" + - "gender" + - "given_name" + - "locale" + - "middle_name" + - "name" + - "nickname" + - "phone_number" + - "phone_number_verified" + - "picture" + - "preferred_username" + - "profile" + - "updated_at" + - "website" + - "zoneinfo" + WriteAttributes: + - "address" + - "birthdate" + - "email" + - "family_name" + - "gender" + - "given_name" + - "locale" + - "middle_name" + - "name" + - "nickname" + - "phone_number" + - "picture" + - "preferred_username" + - "profile" + - "updated_at" + - "website" + - "zoneinfo" + ExplicitAuthFlows: + - "ALLOW_REFRESH_TOKEN_AUTH" + - "ALLOW_USER_SRP_AUTH" + PreventUserExistenceErrors: "ENABLED" + AllowedOAuthFlowsUserPoolClient: false + IdTokenValidity: 1440 + AccessTokenValidity: 1440 + TokenValidityUnits: + AccessToken: "minutes" + IdToken: "minutes" + RefreshToken: "days" + + CognitoUserPoolUser: + Type: "AWS::Cognito::UserPoolUser" + Properties: + Username: !Ref Username + UserPoolId: !Ref CognitoUserPool + UserAttributes: + - + Name: "email_verified" + Value: "true" + - + Name: "email" + Value: !Ref Username + +Outputs: + PublicAppURL: + Description: Public Endpoint + Value: !Join [ "", ["https://", !GetAtt EC2Instance.PublicIp]] + Condition: isPublic + PrivateAppURL: + Description: Private Endpoint + Value: !Join [ "", ["https://", !GetAtt EC2Instance.PrivateIp]] + diff --git a/conf/deploy.sh b/conf/deploy.sh new file mode 100755 index 0000000..4249c81 --- /dev/null +++ b/conf/deploy.sh @@ -0,0 +1,182 @@ +#!/bin/bash + +set -e + +gum style \ + --foreground 212 --border-foreground 212 --border double \ + --align center --width 50 --margin "1 2" --padding "2 4" \ + "DBTop Monitoring Tool" + +export AWS_REGION=$(aws configure get region --profile default) +export AWS_REGION=$(gum input --placeholder ${AWS_REGION} --value ${AWS_REGION}) + +# If AWS_REGION is in Americas I want to use the emoji 🌎 or Europe, Middle East and Africa 🌍 or Asia and Oceania 🌏 +# Function to determine region emoji based on AWS region +get_region_emoji() { + local region=$1 + case $region in + # Americas + us-east-1|us-east-2|us-west-1|us-west-2|ca-central-1|sa-east-1|mx-central-1) + echo "🌎" + ;; + # Europe, Middle East and Africa + eu-central-1|eu-west-1|eu-west-2|eu-west-3|eu-north-1|eu-south-1|me-south-1|af-south-1) + echo "🌍" + ;; + # Asia and Oceania + ap-east-1|ap-southeast-1|ap-southeast-2|ap-southeast-3|ap-northeast-1|ap-northeast-2|ap-northeast-3|ap-south-1) + echo "🌏" + ;; + *) + echo "πŸ—ΊοΈ" + ;; + esac +} + +REGION_EMOJI="$(get_region_emoji ${AWS_REGION}) ${AWS_REGION}" + +gum spin --spinner dot --title "Deploying DBTop to AWS Region: ${REGION_EMOJI}" -- sleep 2 + +Elasticache_RBAC_User_Name="info-ping-user" + +# Check if user exists +if aws elasticache describe-users --user-id ${Elasticache_RBAC_User_Name} --region ${AWS_REGION} >/dev/null 2>&1; then + echo "User ${Elasticache_RBAC_User_Name} already exists in region ${REGION_EMOJI}" +else + [ ! -f password.txt ] && gum input --password > password.txt + if [ ! -f password.txt ] || [ $(cat password.txt | wc -c) -lt 16 ]; then + openssl rand -base64 32 > password.txt + fi + + echo "Creating user ${Elasticache_RBAC_User_Name} in region ${REGION_EMOJI}" + Elasticache_RBAC_User_File="../logs/elasticache-rbac-user.json" + aws elasticache create-user \ + --user-id ${Elasticache_RBAC_User_Name} \ + --user-name ${Elasticache_RBAC_User_Name} \ + --engine valkey \ + --authentication-mode Type=password,Passwords=$(cat password.txt) \ + --access-string "on ~* +info +ping" \ + --region ${AWS_REGION} > ${Elasticache_RBAC_User_File} + cat ${Elasticache_RBAC_User_File} | jq -C . +fi + +STACK_NAME="db-top-solution" +STACK_NAME=$(gum input --placeholder ${STACK_NAME} --value ${STACK_NAME}) +echo "Stack name: ${STACK_NAME}" + +USER_EMAIL="email@example.com" +USER_EMAIL=$(gum input --placeholder ${USER_EMAIL} --value ${USER_EMAIL}) +echo "User email: ${USER_EMAIL}" + +VPC_PARAM="vpc-abc123456789" +VPC_PARAM=$(gum input --placeholder ${VPC_PARAM} --value ${VPC_PARAM}) + +# Check if VPC exists in the specified region +if ! aws ec2 describe-vpcs --vpc-ids ${VPC_PARAM} --region ${AWS_REGION} >/dev/null 2>&1; then + echo "Error: VPC ${VPC_PARAM} does not exist in region ${REGION_EMOJI}" + # Get list of VPCs and format for gum choice + VPC_LIST=$(aws ec2 describe-vpcs --region ${AWS_REGION} --query 'Vpcs[*].[VpcId,Tags[?Key==`Name`].Value | [0],CidrBlock]' --output text | awk '{printf "%s (%s) - %s\n", $1, $2, $3}') + # Let user select VPC using gum choice + VPC_SELECTION=$(echo "${VPC_LIST}" | gum choose) + # Extract VPC ID from selection + VPC_PARAM=$(echo ${VPC_SELECTION} | cut -d' ' -f1) +fi +echo "Selected VPC: ${VPC_PARAM}" + +SUBNET_PARAM="subnet-abc123456789" +SUBNET_PARAM=$(gum input --placeholder ${SUBNET_PARAM} --value ${SUBNET_PARAM}) + +# Check if subnet exists and is public +check_subnet_public() { + local subnet_id=$1 + local vpc_id=$2 + local region=$3 + + # Check if subnet exists and get route table info + if ! aws ec2 describe-subnets --subnet-ids ${subnet_id} --region ${region} >/dev/null 2>&1; then + return 1 + fi + + # Check if subnet has route to internet gateway or NAT gateway + local route_tables=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=${vpc_id}" "Name=association.subnet-id,Values=${subnet_id}" --region ${region} --query 'RouteTables[*].Routes[?DestinationCidrBlock==`0.0.0.0/0`].[GatewayId,NatGatewayId]' --output text) + + if [[ -n "${route_tables}" ]]; then + return 0 + fi + return 1 +} + +# Check if provided subnet is valid and public +if ! check_subnet_public ${SUBNET_PARAM} ${VPC_PARAM} ${AWS_REGION}; then + echo "Error: Subnet ${SUBNET_PARAM} is not public or does not exist in VPC ${VPC_PARAM} region ${REGION_EMOJI}" + + # Get list of public subnets with NAT gateway + SUBNET_LIST=$(aws ec2 describe-subnets \ + --filters "Name=vpc-id,Values=${VPC_PARAM}" \ + --region ${AWS_REGION} \ + --query 'Subnets[*].[SubnetId,Tags[?Key==`Name`].Value | [0],CidrBlock]' \ + --output text | while read subnet_id name cidr; do + if check_subnet_public ${subnet_id} ${VPC_PARAM} ${AWS_REGION}; then + echo "${subnet_id} (${name:-Unnamed}) - ${cidr}" + fi + done) + + # Let user select subnet using gum choice + SUBNET_SELECTION=$(echo "${SUBNET_LIST}" | gum choose) + # Extract subnet ID from selection + SUBNET_PARAM=$(echo ${SUBNET_SELECTION} | cut -d' ' -f1) +fi +echo "Selected Subnet: ${SUBNET_PARAM}" + +INSTANCE_TYPE="t4g.large" +INSTANCE_TYPE=$(gum input --placeholder ${INSTANCE_TYPE} --value ${INSTANCE_TYPE}) + +# Get allowed instance types from CloudFormation template +ALLOWED_INSTANCES=$(cat DBTopMonitoringSolution.yaml | yq -r '.Parameters.InstanceType.AllowedValues[]') + +# Check if provided instance type is in allowed list +if ! echo "${ALLOWED_INSTANCES}" | grep -q "^${INSTANCE_TYPE}$"; then + echo "Error: Instance type ${INSTANCE_TYPE} is not allowed" + # Let user select from allowed instances using gum choice + INSTANCE_TYPE=$(echo "${ALLOWED_INSTANCES}" | gum choose) +fi +echo "Selected Instance Type: ${INSTANCE_TYPE}" + +PUBLIC_ACCESS="true" +SG_INBOUND_ACCESS="0.0.0.0/0" +GITHUB_REPOSITORY="aws-samples" + +# Check if stack already exists +if aws cloudformation describe-stacks --stack-name "${STACK_NAME}" --region ${AWS_REGION} >/dev/null 2>&1; then + echo "Stack ${STACK_NAME} already exists in region ${REGION_EMOJI}" + + # Get stack outputs + STACK_OUTPUTS=$(aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region ${AWS_REGION} \ + --query 'Stacks[0].Outputs[]' \ + --output json) + + # Display stack description + echo "Stack ${STACK_NAME} details:" + echo "${STACK_OUTPUTS}" | jq -r '.[] | "\(.Description): \(.OutputValue)"' + + # Get public URL from outputs + PUBLIC_URL=$(echo "${STACK_OUTPUTS}" | jq -r '.[] | select(.OutputKey=="PublicAppURL") | .OutputValue') + echo -e "\nPublic URL: ${PUBLIC_URL}" +else + echo "Creating stack ${STACK_NAME} in region ${REGION_EMOJI}" + aws cloudformation create-stack \ + --stack-name "${STACK_NAME}" \ + --template-body file://DBTopMonitoringSolution.yaml \ + --parameters \ + ParameterKey=Username,ParameterValue=${USER_EMAIL} \ + ParameterKey=VPCParam,ParameterValue=${VPC_PARAM} \ + ParameterKey=SubnetParam,ParameterValue=${SUBNET_PARAM} \ + ParameterKey=InstanceType,ParameterValue=${INSTANCE_TYPE} \ + ParameterKey=PublicAccess,ParameterValue=${PUBLIC_ACCESS} \ + ParameterKey=SGInboundAccess,ParameterValue=${SG_INBOUND_ACCESS} \ + ParameterKey=GitHubRepository,ParameterValue=${GITHUB_REPOSITORY} \ + --region ${AWS_REGION} \ + --capabilities CAPABILITY_NAMED_IAM > ../logs/stack-${STACK_NAME}.json +fi