AWSTemplateFormatVersion: 2010-09-09 Description: This template deploys a VPC, with a pair of public and private subnets spread across three Availability Zones. In addition to the network components, the following will also be created (AWS Cloud9, Amazon OpenSearch Service and Reverse-Proxy Instance). Parameters: # VPC Variables EnvironmentName: Description: An environment name that is prefixed to resource names Type: String Default: VPC-Observability # Amazon OpenSearch Variables DomainName: Type: String Default: "observability-aos" OpenSearchMasterUserName: Description: Amazon OpenSearch Service - Username Default: "aosadmin" Type: String # EKS Cluster Variables EKSClusterName: Description: 'Please enter the EKS Cluster Name' Type: String Default: "observability-cluster" TemplateRepo: Description: 'Please enter the URL for the EKS Cluster template' Type: String Default: "https://aws-blogs-artifacts-public.s3.amazonaws.com/artifacts/BDB-2223/eks.yaml" Mappings: SubnetConfig: VPC: CIDR: '172.16.0.0/16' PublicSubnet1: CIDR: '172.16.10.0/24' PublicSubnet2: CIDR: '172.16.11.0/24' PublicSubnet3: CIDR: '172.16.12.0/24' PrivateSubnet1: CIDR: '172.16.20.0/24' PrivateSubnet2: CIDR: '172.16.21.0/24' PrivateSubnet3: CIDR: '172.16.22.0/24' AWSInstanceType2Arch: t1.micro: Arch: HVM64 t2.small: Arch: HVM64 t2.medium: Arch: HVM64 t2.large: Arch: HVM64 m1.small: Arch: HVM64 m1.medium: Arch: HVM64 c1.medium: Arch: HVM64 c3.large: Arch: HVM64 c3.xlarge: Arch: HVM64 AWSRegionArch2AMI: us-east-1: HVM64: ami-026b57f3c383c2eec us-east-2: HVM64: ami-0f924dc71d44d23e2 us-west-1: HVM64: ami-09208e69ff3feb1db us-west-2: HVM64: ami-08e2d37b6a0129927 sa-east-1: HVM64: ami-0895310529c333a0c Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "Environment Name" Parameters: - EnvironmentName - Label: default: "OpenSearch Configuration" Parameters: - DomainName - OpenSearchMasterUserName - Label: default: "EKS Configuration" Parameters: - EKSClusterName - TemplateRepo Resources: ######## VPC Template ######## VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Ref EnvironmentName - Key: IsUsedForDeploy Value: "True" InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Ref EnvironmentName InternetGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC PublicSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] CidrBlock: !FindInMap ['SubnetConfig', 'PublicSubnet1', 'CIDR'] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Subnet (AZ1) PublicSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 1, !GetAZs '' ] CidrBlock: !FindInMap ['SubnetConfig', 'PublicSubnet2', 'CIDR'] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Subnet (AZ2) PublicSubnet3: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 2, !GetAZs '' ] CidrBlock: !FindInMap ['SubnetConfig', 'PublicSubnet3', 'CIDR'] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Subnet (AZ3) PrivateSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] CidrBlock: !FindInMap ['SubnetConfig', 'PrivateSubnet1', 'CIDR'] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Subnet (AZ1) - Key: IsUsedForDeploy Value: "True" PrivateSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 1, !GetAZs '' ] CidrBlock: !FindInMap ['SubnetConfig', 'PrivateSubnet2', 'CIDR'] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Subnet (AZ2) - Key: IsUsedForDeploy Value: "True" PrivateSubnet3: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 2, !GetAZs '' ] CidrBlock: !FindInMap ['SubnetConfig', 'PrivateSubnet3', 'CIDR'] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Subnet (AZ3) - Key: IsUsedForDeploy Value: "True" NatGateway1EIP: Type: AWS::EC2::EIP DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway2EIP: Type: AWS::EC2::EIP DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway3EIP: Type: AWS::EC2::EIP DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway1: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway1EIP.AllocationId SubnetId: !Ref PublicSubnet1 NatGateway2: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway2EIP.AllocationId SubnetId: !Ref PublicSubnet2 NatGateway3: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway3EIP.AllocationId SubnetId: !Ref PublicSubnet3 PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Routes DefaultPublicRoute: Type: AWS::EC2::Route DependsOn: InternetGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet1 PublicSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet2 PublicSubnet3RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet3 PrivateRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Routes (AZ1) DefaultPrivateRoute1: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable1 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway1 PrivateSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 PrivateRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Routes (AZ2) DefaultPrivateRoute2: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable2 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway2 PrivateSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable2 SubnetId: !Ref PrivateSubnet2 PrivateRouteTable3: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Routes (AZ3) DefaultPrivateRoute3: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable3 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway3 PrivateSubnet3RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable3 SubnetId: !Ref PrivateSubnet3 IngressSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "VPC Endpoint Ports Required" VpcId: !Ref VPC GroupName: "My SG Group VPC" SecurityGroupIngress: - FromPort: 443 IpProtocol: tcp ToPort: 443 CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] - FromPort: 80 IpProtocol: tcp ToPort: 80 CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] SecurityGroupEgress: - Description: Allow all outbound traffic IpProtocol: "-1" CidrIp: 0.0.0.0/0 CreateVpcEndpointSSM: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 - !Ref PrivateSubnet3 SecurityGroupIds: - !Ref IngressSecurityGroup PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm VpcId: !Ref VPC CreateVpcEndpointSSMMessages: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 - !Ref PrivateSubnet3 SecurityGroupIds: - !Ref IngressSecurityGroup PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages VpcId: !Ref VPC CreateVpcEndpointEC2Messages: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 - !Ref PrivateSubnet3 SecurityGroupIds: - !Ref IngressSecurityGroup PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages VpcId: !Ref VPC ######## OpenSearch Template ######## AWSServiceRoleForAmazonOpenSearchService: Type: 'AWS::IAM::ServiceLinkedRole' Properties: AWSServiceName: es.amazonaws.com OpenSearchIngressSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: "opensearch-ingress-sg" GroupDescription: "Security group for opensearch ingress rule" VpcId: !Ref VPC SecurityGroupIngress: - FromPort: '443' IpProtocol: tcp ToPort: '443' CidrIp: 0.0.0.0/0 SecurityGroupEgress: - Description: Allow all outbound traffic IpProtocol: "-1" CidrIp: 0.0.0.0/0 OpenSearchServiceDomain: Type: 'AWS::OpenSearchService::Domain' DependsOn: - OpenSearchIngressSecurityGroup - AWSServiceRoleForAmazonOpenSearchService Properties: DomainName: Ref: DomainName EngineVersion: OpenSearch_1.3 ClusterConfig: InstanceCount: '1' InstanceType: r6g.large.search DomainEndpointOptions: EnforceHTTPS: true NodeToNodeEncryptionOptions: Enabled: true EncryptionAtRestOptions: Enabled: true EBSOptions: EBSEnabled: true Iops: '0' VolumeSize: '100' VolumeType: 'gp2' AccessPolicies: Version: '2012-10-17' Statement: - Effect: Allow Principal: AWS: '*' Action: 'es:*' Resource: '*' AdvancedOptions: rest.action.multi.allow_explicit_index: true AdvancedSecurityOptions: Enabled: true InternalUserDatabaseEnabled: true MasterUserOptions: MasterUserName: !Ref OpenSearchMasterUserName MasterUserPassword: !Join - "" - - "{{resolve:secretsmanager:" - !Ref AOSMasterPasswordSecret - ":SecretString:password}}" VPCOptions: SubnetIds: - !Ref PrivateSubnet1 SecurityGroupIds: - !Ref OpenSearchIngressSecurityGroup UpdatePolicy: EnableVersionUpgrade: true ######## Reverse Proxy Template ######## IAMRole: Type: AWS::IAM::Role Properties: RoleName: !Sub Linux-SSMRoletoEC2-${AWS::StackName} AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: ec2.amazonaws.com Action: sts:AssumeRole Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - Ref: IAMRole ReverseProxyASG: Type: 'AWS::AutoScaling::AutoScalingGroup' Properties: VPCZoneIdentifier: - !Ref PublicSubnet1 - !Ref PublicSubnet2 LaunchConfigurationName: !Ref ReverseProxyLaunchConfig MinSize: '1' MaxSize: '1' Tags: - Key: Environment Value: Poc PropagateAtLaunch: "true" - Key: IsUsedForDeploy Value: True PropagateAtLaunch: "true" - Key: Name Value: ProxyInstance PropagateAtLaunch: "true" ReverseProxyLaunchConfig: Type: 'AWS::AutoScaling::LaunchConfiguration' Properties: AssociatePublicIpAddress: True IamInstanceProfile: !Ref InstanceProfile ImageId: !FindInMap - AWSRegionArch2AMI - !Ref 'AWS::Region' - !FindInMap - AWSInstanceType2Arch - 't2.small' - Arch UserData: Fn::Base64: !Sub | #!/bin/bash yum update -y yum install jq -y amazon-linux-extras install nginx1.12 openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt -subj /C=US/ST=./L=./O=./CN=.\n cat << EOF > /etc/nginx/conf.d/nginx_opensearch.conf server { listen 443; server_name \$host; rewrite ^/$ https://\$host/_dashboards redirect; # openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt -subj /C=US/ST=./L=./O=./CN=.\n ssl_certificate /etc/nginx/cert.crt; ssl_certificate_key /etc/nginx/cert.key; ssl on; ssl_session_cache builtin:1000 shared:SSL:10m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; location ^~ /_dashboards { # Forward requests to OpenSearch Dashboards proxy_pass https://DOMAIN_ENDPOINT/_dashboards; # Update cookie domain and path proxy_cookie_domain DOMAIN_ENDPOINT \$host; proxy_set_header Accept-Encoding ""; sub_filter_types *; sub_filter DOMAIN_ENDPOINT \$host; sub_filter_once off; # Response buffer settings proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; } } EOF sed -i -e "s/DOMAIN_ENDPOINT/${OpenSearchServiceDomain.DomainEndpoint}/g" /etc/nginx/conf.d/nginx_opensearch.conf systemctl restart nginx.service systemctl enable nginx.service SecurityGroups: - !Ref ReverseProxyInstanceSecurityGroup InstanceType: t2.small ReverseProxyInstanceSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: Enable SSH access SecurityGroupIngress: - IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] - IpProtocol: tcp FromPort: '443' ToPort: '443' CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] - IpProtocol: tcp FromPort: '443' ToPort: '443' CidrIp: 0.0.0.0/0 SecurityGroupEgress: - Description: Allow all outbound traffic IpProtocol: "-1" CidrIp: 0.0.0.0/0 VpcId: !Ref VPC ######## ECR Repositories ######## AnalyticsServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "analytics-service" DatabaseServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "database-service" OrderServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "order-service" InventoryServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "inventory-service" PaymentServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "payment-service" RecommendationServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "recommendation-service" AuthenticationServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "authentication-service" ClientServiceRepository: Type: AWS::ECR::Repository Properties: RepositoryName: "client-service" ######## Cloud9 ######## C9Role: Type: AWS::IAM::Role Properties: RoleName: observabilityworkshop-admin Tags: - Key: Environment Value: observability AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com - ssm.amazonaws.com - eks.amazonaws.com - codebuild.amazonaws.com Action: - sts:AssumeRole - Effect: Allow Principal: AWS: !GetAtt DeployCloudformationStackLambdaRole.Arn Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AdministratorAccess - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore Path: "/" Policies: - PolicyName: Fn::Join: - '' - - C9InstanceDenyPolicy- - Ref: AWS::Region PolicyDocument: Version: '2012-10-17' Statement: - Effect: Deny Action: - cloud9:UpdateEnvironment Resource: "*" - PolicyName: RunKubeCtlCommands PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'eks:*' - 'cloudformation:CreateStack' - 'cloudformation:CreateChangeSet' - 'serverlessrepo:CreateCloudFormationTemplate' - 'serverlessrepo:GetCloudFormationTemplate' - 's3:GetObject' - 'lambda:PublishLayerVersion' - 'lambda:CreateFunction' - 'lambda:GetLayerVersion' - 'lambda:GetFunction' - 'lambda:InvokeFunction' - 'lambda:GetFunctionConfiguration' - 'iam:PassRole' - 'iam:GetRole' - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: '*' - Effect: Allow Action: - 'iam:PassRole' - 'iam:GetRole' Resource: - !GetAtt EKSIAMRole.Arn - !GetAtt DeployCloudformationStackLambdaRole.Arn C9LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" Policies: - PolicyName: Fn::Join: - '' - - C9LambdaPolicy- - Ref: AWS::Region PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - cloudformation:DescribeStacks - cloudformation:DescribeStackEvents - cloudformation:DescribeStackResource - cloudformation:DescribeStackResources Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/*" - Effect: Allow Action: - ec2:AssociateIamInstanceProfile - ec2:ModifyInstanceAttribute - ec2:ReplaceIamInstanceProfileAssociation - ec2:DisassociateIamInstanceProfile Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*" - Effect: Allow Action: - ec2:DescribeInstances - ec2:DescribeIamInstanceProfileAssociations Resource: "*" - Effect: Allow Action: - iam:ListInstanceProfiles Resource: !Sub arn:aws:iam::${AWS::AccountId}:instance-profile/* - Effect: Allow Action: - iam:PassRole Resource: Fn::GetAtt: - C9Role - Arn C9BootstrapInstanceLambda: Type: Custom::C9BootstrapInstanceLambda DependsOn: - C9LambdaExecutionRole Properties: Tags: - Key: Environment Value: observability ServiceToken: Fn::GetAtt: - C9BootstrapInstanceLambdaFunction - Arn REGION: Ref: AWS::Region StackName: Ref: AWS::StackName EnvironmentId: Ref: C9Instance LabIdeInstanceProfileName: Ref: C9InstanceProfile LabIdeInstanceProfileArn: Fn::GetAtt: - C9InstanceProfile - Arn C9BootstrapInstanceLambdaFunction: Type: AWS::Lambda::Function Properties: Tags: - Key: Environment Value: AWS Example Handler: index.lambda_handler Role: Fn::GetAtt: - C9LambdaExecutionRole - Arn Runtime: python3.9 MemorySize: 256 Timeout: 600 Code: ZipFile: | from __future__ import print_function import boto3 import json import os import time import traceback import cfnresponse import logging logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) def lambda_handler(event, context): logger.info('event: {}'.format(event)) logger.info('context: {}'.format(context)) responseData = {} if event['RequestType'] == 'Create': try: # Open AWS clients ec2 = boto3.client('ec2') # Get the InstanceId of the Cloud9 IDE instance = ec2.describe_instances(Filters=[{'Name': 'tag:Name','Values': ['aws-cloud9-observabilityworkshop'+'-'+event['ResourceProperties']['EnvironmentId']]}])['Reservations'][0]['Instances'][0] logger.info('instance: {}'.format(instance)) # Create the IamInstanceProfile request object iam_instance_profile = { 'Arn': event['ResourceProperties']['LabIdeInstanceProfileArn'], 'Name': event['ResourceProperties']['LabIdeInstanceProfileName'] } logger.info('iam_instance_profile: {}'.format(iam_instance_profile)) # Wait for Instance to become ready before adding Role instance_state = instance['State']['Name'] logger.info('instance_state: {}'.format(instance_state)) while instance_state != 'running': time.sleep(5) instance_state = ec2.describe_instances(InstanceIds=[instance['InstanceId']]) logger.info('instance_state: {}'.format(instance_state)) # Disassociate existing IAM instance profile instance_profiles = ec2.describe_iam_instance_profile_associations() logger.info('instance_profiles: {}'.format(instance_profiles)) for profile in instance_profiles['IamInstanceProfileAssociations']: if (profile['InstanceId'] == instance['InstanceId']) and profile['State'] == 'associated': logger.info(profile) disassociation = ec2.disassociate_iam_instance_profile(AssociationId=profile['AssociationId']) logger.info('Disassociated existing instance profile attached to the EC2 instance: {}'.format(disassociation)) time.sleep(5) # attach instance profile response = ec2.associate_iam_instance_profile(IamInstanceProfile=iam_instance_profile, InstanceId=instance['InstanceId']) logger.info('response - associate_iam_instance_profile: {}'.format(response)) responseData = {'Success': 'Started bootstrapping for instance: '+instance['InstanceId']} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, 'CustomResourcePhysicalID') except Exception as e: logger.error(e, exc_info=True) # responseData = {'Error': traceback.format_exc(e)} responseData = {'Error':'There was a problem associating IAM profile to the Cloud9 Instance'} cfnresponse.send(event, context, cfnresponse.FAILED, responseData, 'CustomResourcePhysicalID') else: responseData = {'Success': 'Update or delete event'} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, 'CustomResourcePhysicalID') ################## GENERATE OPENSEARCH PASSWORD ################### AOSMasterPasswordSecret: Type: AWS::SecretsManager::Secret Properties: Description: This secret has a dynamically generated secret password. GenerateSecretString: SecretStringTemplate: !Join [ '', [ '{"username": "', !Ref OpenSearchMasterUserName, '"}' ] ] GenerateStringKey: "password" PasswordLength: 10 ExcludeCharacters: "\" ' ( ) * + , - . / : ; < = > ! # ? @ [ \\ ] ^ _ ` { | } ~" RetrieveAOSPasswordLambdaPolicy: Type: AWS::IAM::ManagedPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Effect: Allow Resource: arn:aws:logs:*:*:* Sid: AllowCWLogsWrite - Action: - secretsmanager:GetSecretValue Effect: Allow Resource: !Ref AOSMasterPasswordSecret RetrieveAOSPasswordLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: RetrieveAOSPasswordLambdaPolicy Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - !Ref RetrieveAOSPasswordLambdaPolicy Path: / RetrieveAOSPasswordLambdaFunction: Type: AWS::Lambda::Function DependsOn: AOSMasterPasswordSecret Properties: Handler: index.lambda_handler Role: !GetAtt RetrieveAOSPasswordLambdaExecutionRole.Arn Runtime: python3.9 Timeout: 120 Code: ZipFile: | import json import boto3 import base64 import os import cfnresponse from botocore.exceptions import ClientError SECRET_ARN = os.getenv('SECRET_ARN') REGION = os.getenv('REGION') def lambda_handler(event, context): # Create a Secrets Manager client session = boto3.session.Session() client = session.client( service_name='secretsmanager', region_name=REGION ) secret = "" try: get_secret_value_response = client.get_secret_value( SecretId=SECRET_ARN ) except ClientError as err: print(err) cfnresponse.send(event, context, cfnresponse.FAILED, err) else: # Decrypts secret using the associated KMS key. # Depending on whether the secret is a string or binary, one of these fields will be populated. if 'SecretString' in get_secret_value_response: secret = get_secret_value_response['SecretString'] else: decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary']) password_secret = json.loads(secret) responseData = {"OpenSearchMasterPassword": password_secret["password"]} print(responseData) if responseData: cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) else: cfnresponse.send(event, context, cfnresponse.FAILED, "Internal Error") Environment: Variables: SECRET_ARN: !Ref AOSMasterPasswordSecret REGION: !Ref AWS::Region RetrieveAOSPassword: Type: Custom::RetrieveAOSPassword DependsOn: RetrieveAOSPasswordLambdaFunction Properties: ServiceToken: Fn::GetAtt: RetrieveAOSPasswordLambdaFunction.Arn ################## SSM BOOTSRAP HANDLER ############### C9OutputBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: AccessControl: Private BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true C9OutputBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref C9OutputBucket PolicyDocument: Version: 2012-10-17 Statement: - Action: - 's3:GetObject' - 's3:PutObject' - 's3:PutObjectAcl' Effect: Allow Resource: !Join - '' - - 'arn:aws:s3:::' - !Ref C9OutputBucket - /* Principal: AWS: Fn::GetAtt: - C9LambdaExecutionRole - Arn C9SSMDocument: Type: AWS::SSM::Document Properties: Tags: - Key: Environment Value: observability DocumentType: Command Content: schemaVersion: '2.2' description: Bootstrap Cloud9 Instance mainSteps: - action: aws:runShellScript name: C9bootstrap inputs: runCommand: - "#!/bin/bash" - date - echo LANG=en_US.utf-8 >> /etc/environment - echo LC_ALL=en_US.UTF-8 >> /etc/environment - . /home/ec2-user/.bashrc - yum -y remove aws-cli; yum -y install sqlite telnet jq strace tree gcc glibc-static python3 python3-pip gettext bash-completion - echo '=== CONFIGURE default python version ===' - PATH=$PATH:/usr/bin - alternatives --set python /usr/bin/python3.9 - echo '=== INSTALL and CONFIGURE default software components ===' - sudo -H -u ec2-user bash -c "pip install --user -U boto boto3 botocore awscli aws-sam-cli" - echo '=== INSTALL Kubectl ===' - curl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.21.2/2021-07-05/bin/linux/amd64/kubectl - sudo chmod +x kubectl && sudo mv kubectl /usr/local/bin/ - sudo echo "source <(kubectl completion bash)" >> ~/.bashrc - echo '=== INSTALL eksctl ===' - curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp - sudo mv /tmp/eksctl /usr/local/bin - echo '=== Resizing the Instance volume' - !Sub SIZE=100 - !Sub REGION=${AWS::Region} - | TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") INSTANCEID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) VOLUMEID=$(aws ec2 describe-instances \ --instance-id $INSTANCEID \ --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ --output text --region $REGION) aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE --region $REGION while [ \ "$(aws ec2 describe-volumes-modifications \ --volume-id $VOLUMEID \ --filters Name=modification-state,Values="optimizing","completed" \ --query "length(VolumesModifications)"\ --output text --region $REGION)" != "1" ]; do sleep 1 done if [ $(readlink -f /dev/xvda) = "/dev/xvda" ] then sudo growpart /dev/xvda 1 STR=$(cat /etc/os-release) SUB="VERSION_ID=\"2\"" if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/xvda1 fi else sudo growpart /dev/nvme0n1 1 STR=$(cat /etc/os-release) SUB="VERSION_ID=\"2\"" if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/nvme0n1p1 fi fi - echo "Bootstrap completed with return code $?" C9BootstrapAssociation: Type: AWS::SSM::Association Properties: Name: !Ref C9SSMDocument OutputLocation: S3Location: OutputS3BucketName: !Ref C9OutputBucket OutputS3KeyPrefix: bootstrapoutput Targets: - Key: tag:SSMBootstrap Values: - Active C9InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - Ref: C9Role C9Instance: DependsOn: C9BootstrapAssociation Type: AWS::Cloud9::EnvironmentEC2 Properties: Description: AWS Cloud9 instance for observability SubnetId: !Ref PublicSubnet1 #ConnectionType: CONNECT_SSM AutomaticStopTimeMinutes: 3600 InstanceType: t3.medium Name: observabilityworkshop Tags: - Key: SSMBootstrap Value: Active - Key: Environment Value: observability ##EKS Cluster EKSIAMRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - eks.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy - arn:aws:iam::aws:policy/AmazonEKSServicePolicy EKSNodesIAMRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - eks.amazonaws.com - ec2.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy - arn:aws:iam::aws:policy/ElasticLoadBalancingFullAccess DeployCloudformationStackLambdaRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: AllowRolePassAndCF PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' - 'iam:PassRole' - 'iam:GetRole' - 'cloudformation:CreateStack' - 'cloudformation:CreateChangeSet' - 'eks:DescribeCluster' Resource: '*' ClusterControlPlaneSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Cluster communication with worker nodes VpcId: !Ref VPC TriggerStack: Type: "Custom::TriggerStack" DependsOn: DeployCloudformationStack Properties: ServiceToken: !GetAtt DeployCloudformationStack.Arn cfStackName: "eksDeploy" assumeRoleARN: !GetAtt C9Role.Arn templateUrl: !Ref TemplateRepo stackParameters: - ParameterKey: EksClusterName ParameterValue: !Ref EKSClusterName - ParameterKey: EksClusterRole ParameterValue: !GetAtt EKSIAMRole.Arn - ParameterKey: EksSubnet1 ParameterValue: !Ref PublicSubnet1 - ParameterKey: EksSubnet2 ParameterValue: !Ref PublicSubnet2 - ParameterKey: EksSubnet3 ParameterValue: !Ref PublicSubnet3 - ParameterKey: EksNodeSubnet1 ParameterValue: !Ref PrivateSubnet1 - ParameterKey: EksNodeSubnet2 ParameterValue: !Ref PrivateSubnet2 - ParameterKey: EksNodeSubnet3 ParameterValue: !Ref PrivateSubnet3 - ParameterKey: EksNodesRole ParameterValue: !GetAtt EKSNodesIAMRole.Arn DeployCloudformationStack: Type: AWS::Lambda::Function Properties: Description: Deploy cloudformation stack Handler: index.lambda_handler Runtime: python3.8 Role: !GetAtt DeployCloudformationStackLambdaRole.Arn MemorySize: 128 Timeout: 30 Code: ZipFile: | import cfnresponse import json, os, boto3, logging from botocore.exceptions import ClientError def lambda_handler(event, context): print("Received event: " + json.dumps(event, indent=2)) payload = "" result = cfnresponse.SUCCESS logger = logging.getLogger() logger.setLevel(logging.INFO) try: if event['RequestType'] == 'Create': payload = deployStack(event['ResourceProperties']) except ClientError as e: logger.error('Error: %s', e) result = cfnresponse.FAILED cfnresponse.send(event, context, result, payload) def deployStack(input): sts = boto3.client('sts').assume_role(RoleArn=input['assumeRoleARN'],RoleSessionName="lambda_assume_role") client = boto3.client('cloudformation', aws_access_key_id=sts['Credentials']['AccessKeyId'], aws_secret_access_key=sts['Credentials']['SecretAccessKey'], aws_session_token=sts['Credentials']['SessionToken'] ) response = client.create_stack(StackName=input['cfStackName'], TemplateURL=input['templateUrl'],Parameters=input['stackParameters'], TimeoutInMinutes=60, Capabilities=['CAPABILITY_IAM','CAPABILITY_NAMED_IAM','CAPABILITY_AUTO_EXPAND']) ## Lambda Get PublicIP Information GetEC2PublicIP: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import json import boto3 import logging import urllib3 import time http = urllib3.PoolManager() logger = logging.getLogger(__name__) logging.getLogger().setLevel(logging.INFO) SUCCESS = "SUCCESS" FAILED = "FAILED" time.sleep(15) def lambda_handler(event, context): global arn logger.info('Event: %s' % json.dumps(event)) responseData={} try: if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': print("Request Type:",event['RequestType']) GetPublicIP=event['ResourceProperties']['GetPublicIP'] client = boto3.client('ec2') response = client.describe_instances( Filters=[ { 'Name': 'tag:IsUsedForDeploy', 'Values': ['true']} ] ) for r in response['Reservations']: for i in r['Instances']: PublicIpAddress = (i['PublicIpAddress']) print (PublicIpAddress) responseData={'PublicIpAddress':PublicIpAddress} print("Sending CFN") responseStatus = 'SUCCESS' except Exception as e: print('Failed to process:', e) responseStatus = 'FAILURE' responseData = {'Failure': 'Check Logs.'} send(event, context, responseStatus, responseData) def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): responseUrl = event['ResponseURL'] print(responseUrl) responseBody = {'Status': responseStatus, 'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name, 'PhysicalResourceId': physicalResourceId or context.log_stream_name, 'StackId': event['StackId'], 'RequestId': event['RequestId'], 'LogicalResourceId': event['LogicalResourceId'], 'Data': responseData} json_responseBody = json.dumps(responseBody) print("Response body:\n" + json_responseBody) headers = { 'content-type' : '', 'content-length' : str(len(json_responseBody)) } try: response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) print("Status code:", response.status) except Exception as e: print("send(..) failed executing http.request(..):", e) FunctionName: 'EC2ASG-GetPublicIpAddress' Handler: "index.lambda_handler" Timeout: 30 Role: !GetAtt 'LambdaRole.Arn' Runtime: python3.9 Lambdatrigger: Type: 'Custom::GetEC2PublicIP' DependsOn: ReverseProxyASG Properties: ServiceToken: !GetAtt 'GetEC2PublicIP.Arn' GetPublicIP: !Ref GetEC2PublicIP LambdaRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: F3 reason: "Required for GetPublicIP" - id: W11 reason: "Required for GetPublicIP" Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: "lambda-logs" PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'ec2:Describe*' - 'ec2:List*' Resource: '*' - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - "arn:aws:logs:*:*:*" Outputs: Cloud9IDE: Value: Fn::Join: - '' - - https:// - Ref: AWS::Region - ".console.aws.amazon.com/cloud9/ide/" - Ref: C9Instance - "?region=" - Ref: AWS::Region Export: Name: Cloud9IDE AOSDomainArn: Value: 'Fn::GetAtt': - OpenSearchServiceDomain - Arn Export: Name: AOSDomainArn AOSDomainEndpoint: Value: 'Fn::GetAtt': - OpenSearchServiceDomain - DomainEndpoint Export: Name: AOSDomainEndpoint AOSDomainUserName: Value: !Ref OpenSearchMasterUserName Export: Name: AOSDomainUserName AOSDomainPassword: Value: !GetAtt RetrieveAOSPassword.OpenSearchMasterPassword Export: Name: AOSDomainPassword AOSDashboardsPublicIP: Description: Proxy (Public IP) for Amazon Opensearch Dashboards Value: Fn::Join: - '' - - https:// - !GetAtt Lambdatrigger.PublicIpAddress - /_dashboards Export: Name: AOSDashboardsPublicIP