diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24f1e68..3de97f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: '^docs/|/migrations/|devcontainer.json|/aws_cloudform.json|^rds_redis_ec2_config.yml' +exclude: '^docs/|/migrations/|devcontainer.json|/aws_cloudform.json|^rds_*.yml' default_stages: [commit] default_language_version: diff --git a/README.md b/README.md index fa4d87e..0550573 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,20 @@ cd yeastregulatorydb celery -A config.celery_app worker -B -l info ``` +## Github CI + +To run the CI that is in this repo, you need to transform the `.envs` directory +into a binary string and save it as a secret. This can be done like so: + +```bash +tar -czvf envs.tar.gz .envs +base64 envs.tar.gz > .tmp.txt +``` + +Then, go to the settings of the repo, and add a secret called `ENV_FILE` with +the string in .tmp.txt. Make sure that and the .tar.gz does not get pushed to +git. + ## Deployment The following details how to deploy this application. diff --git a/compose/production/django/entrypoint b/compose/production/django/entrypoint index f15d0b5..08222b1 100644 --- a/compose/production/django/entrypoint +++ b/compose/production/django/entrypoint @@ -4,12 +4,9 @@ set -o errexit set -o pipefail set -o nounset - - # N.B. If only .env files supported variable expansion... export CELERY_BROKER_URL="redis://${REDIS_HOST}:${REDIS_PORT}/0" - if [ -z "${POSTGRES_USER}" ]; then base_postgres_image_default_user='postgres' export POSTGRES_USER="${base_postgres_image_default_user}" @@ -22,26 +19,39 @@ import time import psycopg -suggest_unrecoverable_after = 30 -start = time.time() - -while True: - try: - psycopg.connect( - dbname="${POSTGRES_DB}", - user="${POSTGRES_USER}", - password="${POSTGRES_PASSWORD}", - host="${POSTGRES_HOST}", - port="${POSTGRES_PORT}", - ) - break - except psycopg.OperationalError as error: - sys.stderr.write("Waiting for PostgreSQL to become available...\n") - - if time.time() - start > suggest_unrecoverable_after: - sys.stderr.write(" This is taking longer than expected. The following exception may be indicative of an unrecoverable error: '{}'\n".format(error)) - - time.sleep(1) +def database_exists(conn_params, dbname): + with psycopg.connect(**conn_params) as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (dbname,)) + return cur.fetchone() is not None + +def create_database(conn_params, dbname): + with psycopg.connect(**conn_params) as conn: + conn.autocommit = True + with conn.cursor() as cur: + cur.execute(f"CREATE DATABASE \"{dbname}\"") + +conn_params = { + "dbname": "postgres", # connect to the default database to check/create + "user": "${POSTGRES_USER}", + "password": "${POSTGRES_PASSWORD}", + "host": "${POSTGRES_HOST}", + "port": "${POSTGRES_PORT}" +} + +dbname = "${POSTGRES_DB}" + +if not database_exists(conn_params, dbname): + print("Database does not exist. Creating database: {}".format(dbname)) + create_database(conn_params, dbname) +else: + print("Database {} already exists.".format(dbname)) + +# Now connect to the target database +conn_params["dbname"] = dbname +with psycopg.connect(**conn_params) as conn: + print('Connected to the database successfully') + END >&2 echo 'PostgreSQL is available' diff --git a/compose/production/django/entrypoint_modified b/compose/production/django/entrypoint_modified deleted file mode 100644 index 08222b1..0000000 --- a/compose/production/django/entrypoint_modified +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - -# N.B. If only .env files supported variable expansion... -export CELERY_BROKER_URL="redis://${REDIS_HOST}:${REDIS_PORT}/0" - -if [ -z "${POSTGRES_USER}" ]; then - base_postgres_image_default_user='postgres' - export POSTGRES_USER="${base_postgres_image_default_user}" -fi -export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" - -python << END -import sys -import time - -import psycopg - -def database_exists(conn_params, dbname): - with psycopg.connect(**conn_params) as conn: - with conn.cursor() as cur: - cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (dbname,)) - return cur.fetchone() is not None - -def create_database(conn_params, dbname): - with psycopg.connect(**conn_params) as conn: - conn.autocommit = True - with conn.cursor() as cur: - cur.execute(f"CREATE DATABASE \"{dbname}\"") - -conn_params = { - "dbname": "postgres", # connect to the default database to check/create - "user": "${POSTGRES_USER}", - "password": "${POSTGRES_PASSWORD}", - "host": "${POSTGRES_HOST}", - "port": "${POSTGRES_PORT}" -} - -dbname = "${POSTGRES_DB}" - -if not database_exists(conn_params, dbname): - print("Database does not exist. Creating database: {}".format(dbname)) - create_database(conn_params, dbname) -else: - print("Database {} already exists.".format(dbname)) - -# Now connect to the target database -conn_params["dbname"] = dbname -with psycopg.connect(**conn_params) as conn: - print('Connected to the database successfully') - -END - ->&2 echo 'PostgreSQL is available' - -exec "$@" diff --git a/compose/production/django/entrypoint_orig b/compose/production/django/entrypoint_orig new file mode 100644 index 0000000..f15d0b5 --- /dev/null +++ b/compose/production/django/entrypoint_orig @@ -0,0 +1,49 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + + +# N.B. If only .env files supported variable expansion... +export CELERY_BROKER_URL="redis://${REDIS_HOST}:${REDIS_PORT}/0" + + +if [ -z "${POSTGRES_USER}" ]; then + base_postgres_image_default_user='postgres' + export POSTGRES_USER="${base_postgres_image_default_user}" +fi +export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +python << END +import sys +import time + +import psycopg + +suggest_unrecoverable_after = 30 +start = time.time() + +while True: + try: + psycopg.connect( + dbname="${POSTGRES_DB}", + user="${POSTGRES_USER}", + password="${POSTGRES_PASSWORD}", + host="${POSTGRES_HOST}", + port="${POSTGRES_PORT}", + ) + break + except psycopg.OperationalError as error: + sys.stderr.write("Waiting for PostgreSQL to become available...\n") + + if time.time() - start > suggest_unrecoverable_after: + sys.stderr.write(" This is taking longer than expected. The following exception may be indicative of an unrecoverable error: '{}'\n".format(error)) + + time.sleep(1) +END + +>&2 echo 'PostgreSQL is available' + +exec "$@" diff --git a/rds_redis_backend.yml b/rds_redis_backend.yml deleted file mode 100644 index 553e2de..0000000 --- a/rds_redis_backend.yml +++ /dev/null @@ -1,443 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: CloudFormation template for PostgreSQL RDS, Redis ElastiCache, and Ubuntu EC2 instance. - -Parameters: - MyVPC: - Type: AWS::EC2::VPC::Id - Description: The ID of the VPC, eg vpc-0a1234567890abcdef - - AppTagValue: - Type: String - Description: The value of the app tag to apply to resources - Default: myapp - - DBName: - Type: String - Description: The name of the database. Eg, yeastregulatorydb - - PostgresPassword: - Type: String - Description: The master password for the RDS instance. Must be at least 8 characters long. - NoEcho: true - - PostgresUser: - Type: String - Description: The master username for the RDS instance - Default: postgres - - PostgresVersion: - Type: String - Description: The version of Postgres to use for the RDS instance - Default: 15.2 - - PostgresVersionFamily: - Type: String - Description: The version family of Postgres to use for the RDS instance - Default: postgres15 - - RdsInstanceType: - Type: String - Description: The instance type for the RDS instance - Default: db.t3.micro - - RdsAllocatedStorage: - Type: String - Description: The allocated storage for the RDS instance - Default: 20 - - ElasticacheInstanceType: - Type: String - Description: The instance type for the ElastiCache Redis instance - Default: cache.t2.micro - - MyInternetGateway: - Type: String - Description: The ID of the internet gateway, eg igw-0a1234567890abcdef - - SubnetACidrBlock: - Type: String - Default: 172.31.64.0/20 - Description: The CIDR block for Subnet A. - - PrivateSubnetACidrBlock: - Type: String - Default: 172.31.50.0/24 - Description: The CIDR block for Private Subnet A. - - SubnetBCidrBlock: - Type: String - Default: 172.31.80.0/20 - Description: The CIDR block for Subnet B. - - PrivateSubnetBCidrBlock: - Type: String - Default: 172.31.51.0/24 - Description: The CIDR block for Private Subnet B. - - SubnetCCidrBlock: - Type: String - Default: 172.31.96.0/20 - Description: The CIDR block for Subnet C. - - PrivateSubnetCCidrBlock: - Type: String - Default: 172.31.52.0/24 - Description: The CIDR block for Private Subnet C - -Resources: - - SubnetA: - Type: AWS::EC2::Subnet - DependsOn: - - PublicRoute - Properties: - VpcId: !Ref MyVPC - CidrBlock: !Ref SubnetACidrBlock - AvailabilityZone: us-east-2a - Tags: - - Key: app - Value: !Ref AppTagValue - - SubnetB: - Type: AWS::EC2::Subnet - DependsOn: - - PublicRoute - Properties: - VpcId: !Ref MyVPC - CidrBlock: !Ref SubnetBCidrBlock - AvailabilityZone: us-east-2b - Tags: - - Key: app - Value: !Ref AppTagValue - - SubnetC: - Type: AWS::EC2::Subnet - DependsOn: - - PublicRoute - Properties: - VpcId: !Ref MyVPC - CidrBlock: !Ref SubnetCCidrBlock - AvailabilityZone: us-east-2c - Tags: - - Key: app - Value: !Ref AppTagValue - - PrivateSubnetA: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref MyVPC - CidrBlock: !Ref PrivateSubnetACidrBlock - AvailabilityZone: us-east-2a - Tags: - - Key: app - Value: !Ref AppTagValue - - PrivateSubnetB: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref MyVPC - CidrBlock: !Ref PrivateSubnetBCidrBlock - AvailabilityZone: us-east-2b - Tags: - - Key: app - Value: !Ref AppTagValue - - PrivateSubnetC: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref MyVPC - CidrBlock: !Ref PrivateSubnetCCidrBlock - AvailabilityZone: us-east-2c - Tags: - - Key: app - Value: !Ref AppTagValue - - PrivateRouteTable: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref MyVPC - Tags: - - Key: Name - Value: PrivateRouteTable - - MyPublicRouteTable: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref MyVPC - Tags: - - Key: app - Value: !Ref AppTagValue - - PublicRoute: - Type: AWS::EC2::Route - DependsOn: - - MyPublicRouteTable - Properties: - RouteTableId: !Ref MyPublicRouteTable - DestinationCidrBlock: 0.0.0.0/0 - GatewayId: !Ref MyInternetGateway - - SubnetRouteTableAssociationA: - Type: AWS::EC2::SubnetRouteTableAssociation - DependsOn: - - MyPublicRouteTable - - SubnetA - Properties: - SubnetId: !Ref SubnetA - RouteTableId: !Ref MyPublicRouteTable - - SubnetRouteTableAssociationB: - Type: AWS::EC2::SubnetRouteTableAssociation - DependsOn: - - MyPublicRouteTable - - SubnetB - Properties: - SubnetId: !Ref SubnetB - RouteTableId: !Ref MyPublicRouteTable - - SubnetRouteTableAssociationC: - Type: AWS::EC2::SubnetRouteTableAssociation - DependsOn: - - MyPublicRouteTable - - SubnetC - Properties: - SubnetId: !Ref SubnetC - RouteTableId: !Ref MyPublicRouteTable - - AssociationPrivateSubnetA: - Type: AWS::EC2::SubnetRouteTableAssociation - DependsOn: - - PrivateRouteTable - - PrivateSubnetA - Properties: - SubnetId: !Ref PrivateSubnetA - RouteTableId: !Ref PrivateRouteTable - - AssociationPrivateSubnetB: - Type: AWS::EC2::SubnetRouteTableAssociation - DependsOn: - - PrivateRouteTable - - PrivateSubnetB - Properties: - SubnetId: !Ref PrivateSubnetB - RouteTableId: !Ref PrivateRouteTable - - AssociationPrivateSubnetC: - Type: AWS::EC2::SubnetRouteTableAssociation - DependsOn: - - PrivateRouteTable - - PrivateSubnetC - Properties: - SubnetId: !Ref PrivateSubnetC - RouteTableId: !Ref PrivateRouteTable - - DjangoSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: Security group for Django ECS service allowing HTTP and HTTPS traffic - VpcId: !Ref MyVPC - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 80 - ToPort: 80 - CidrIp: 0.0.0.0/0 - - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - CidrIp: 0.0.0.0/0 - Tags: - - Key: app - Value: !Ref AppTagValue - - RedisSecurityGroup: - Type: AWS::EC2::SecurityGroup - DependsOn: - - DjangoSecurityGroup - Properties: - GroupDescription: Security group for Redis service - VpcId: !Ref MyVPC - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 6379 - ToPort: 6379 - SourceSecurityGroupId: !Ref DjangoSecurityGroup - Tags: - - Key: app - Value: !Ref AppTagValue - - MyDBSecurityGroup: - Type: AWS::EC2::SecurityGroup - DependsOn: - - DjangoSecurityGroup - Properties: - GroupDescription: Allow access to PostgreSQL - VpcId: !Ref MyVPC - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 5432 - ToPort: 5432 - SourceSecurityGroupId: !Ref DjangoSecurityGroup - Tags: - - Key: app - Value: !Ref AppTagValue - - MyCacheSecurityGroup: - Type: AWS::EC2::SecurityGroup - DependsOn: - - DjangoSecurityGroup - Properties: - GroupDescription: Allow access to Redis - VpcId: !Ref MyVPC - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 6379 - ToPort: 6379 - SourceSecurityGroupId: !Ref DjangoSecurityGroup - Tags: - - Key: app - Value: !Ref AppTagValue - - MyCustomParameterGroup: - Type: AWS::RDS::DBParameterGroup - Properties: - Description: Custom parameter group for my DB - Family: !Ref PostgresVersionFamily - Parameters: - max_connections: "200" - Tags: - - Key: app - Value: !Ref AppTagValue - - MyDBProxyRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Service: rds.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: RDSProxyPolicy - PolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - - secretsmanager:DescribeSecret - Resource: '*' - Tags: - - Key: app - Value: !Ref AppTagValue - - MyDBSecret: - Type: AWS::SecretsManager::Secret - DependsOn: MyDBInstance - Properties: - Name: MyDBSecret - Description: RDS database credentials - SecretString: !Sub '{"username": "${PostgresUser}", "password": "${PostgresPassword}"}' - Tags: - - Key: app - Value: !Ref AppTagValue - - MyDBProxy: - Type: AWS::RDS::DBProxy - DependsOn: - - MyDBProxyRole - - MyDBSecret - - MyDBSecurityGroup - Properties: - DBProxyName: mydbproxy - EngineFamily: POSTGRESQL - Auth: - - AuthScheme: SECRETS - IAMAuth: DISABLED - SecretArn: !Ref MyDBSecret - RoleArn: !GetAtt MyDBProxyRole.Arn - VpcSecurityGroupIds: - - !Ref MyDBSecurityGroup - VpcSubnetIds: - - !Ref SubnetA - - !Ref SubnetB - - !Ref SubnetC - RequireTLS: false - Tags: - - Key: app - Value: !Ref AppTagValue - - MyDBSubnetGroup: - Type: AWS::RDS::DBSubnetGroup - Properties: - DBSubnetGroupDescription: "My DB Subnet Group" - SubnetIds: - - !Ref PrivateSubnetA - - !Ref PrivateSubnetB - - !Ref PrivateSubnetC - - MyDBInstance: - Type: AWS::RDS::DBInstance - DependsOn: - - MyDBSubnetGroup - - MyCustomParameterGroup - Properties: - DBName: !Ref DBName - AllocatedStorage: !Ref RdsAllocatedStorage - DBInstanceClass: !Ref RdsInstanceType - Engine: postgres - EngineVersion: !Ref PostgresVersion - MasterUsername: !Ref PostgresUser - MasterUserPassword: !Ref PostgresPassword - BackupRetentionPeriod: 3 - DBSubnetGroupName: !Ref MyDBSubnetGroup - VPCSecurityGroups: - - !Ref MyDBSecurityGroup - DBParameterGroupName: !Ref MyCustomParameterGroup - Tags: - - Key: app - Value: !Ref AppTagValue - - MyElastiCacheSubnetGroup: - Type: AWS::ElastiCache::SubnetGroup - Properties: - Description: "Subnet group for ElastiCache" - SubnetIds: - - !Ref PrivateSubnetA - - !Ref PrivateSubnetB - - !Ref PrivateSubnetC - Tags: - - Key: app - Value: !Ref AppTagValue - - MyElastiCacheRedis: - Type: AWS::ElastiCache::CacheCluster - DependsOn: - - MyCacheSecurityGroup - - RedisSecurityGroup - - MyElastiCacheSubnetGroup - Properties: - CacheNodeType: !Ref ElasticacheInstanceType - Engine: redis - NumCacheNodes: 1 - VpcSecurityGroupIds: - - !Ref MyCacheSecurityGroup - - !Ref RedisSecurityGroup - CacheSubnetGroupName: !Ref MyElastiCacheSubnetGroup - Tags: - - Key: app - Value: !Ref AppTagValue - -Outputs: - RDSProxyEndpoint: - Description: Endpoint of the RDS Proxy - Value: !GetAtt MyDBProxy.Endpoint - - ElastiCacheEndpoint: - Description: Endpoint of the ElastiCache Redis - Value: !GetAtt MyElastiCacheRedis.RedisEndpoint.Address - - ElastiCachePort: - Description: Port of the ElastiCache Redis - Value: !GetAtt MyElastiCacheRedis.RedisEndpoint.Port diff --git a/rds_redis_ec2_config.yml b/rds_redis_ec2_config.yml deleted file mode 100644 index 29a25bb..0000000 --- a/rds_redis_ec2_config.yml +++ /dev/null @@ -1,152 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: CloudFormation template for PostgreSQL RDS, Redis ElastiCache, and Ubuntu EC2 instance. - -Resources: - MyCustomParameterGroup: - Type: AWS::RDS::DBParameterGroup - Properties: - Description: Custom parameter group for my DB - Family: postgres13 # Adjust based on your PostgreSQL version - Parameters: - max_connections: "200" # Example: Increase max connections - - MyDBInstance: - Type: AWS::RDS::DBInstance - DependsOn: - - MyDBSecurityGroup - Properties: - DBName: mydatabase - AllocatedStorage: 20 - DBInstanceClass: db.t3.micro - Engine: postgres - MasterUsername: postgres - MasterUserPassword: Jex19UIFCmM2u6ZhRKZd - BackupRetentionPeriod: 3 - VPCSecurityGroups: - - !Ref MyDBSecurityGroup - DBParameterGroupName: !Ref MyCustomParameterGroup # Associate the custom parameter group - - MyDBProxy: - Type: AWS::RDS::DBProxy - Properties: - DBProxyName: mydbproxy - EngineFamily: POSTGRESQL - Auth: - - AuthScheme: SECRETS - IAMAuth: DISABLED - SecretArn: !GetAtt MyDBSecret.Arn - RoleArn: !GetAtt MyDBProxyRole.Arn - VpcSecurityGroupIds: - - !Ref MyDBSecurityGroup - VpcSubnetIds: - - !Ref MySubnet1 - - !Ref MySubnet2 - RequireTLS: false - - MyDBProxyRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: rds.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: RDSProxyPolicy - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - - secretsmanager:DescribeSecret - Resource: '*' - - MyDBSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: MyDBSecret - Description: "RDS database credentials" - SecretString: !Sub '{"username":"${MyDBInstance.MasterUsername}","password":"Jex19UIFCmM2u6ZhRKZd","engine":"postgres","host":"${MyDBInstance.Endpoint.Address}","port":"5432","dbClusterIdentifier":"${MyDBInstance.DBInstanceIdentifier}"}' - - - MyElastiCacheRedis: - Type: AWS::ElastiCache::CacheCluster - DependsOn: - - MyCacheSecurityGroup - Properties: - CacheNodeType: cache.t2.micro - Engine: redis - NumCacheNodes: 1 - VpcSecurityGroupIds: - - !Ref MyCacheSecurityGroup - - # MyEC2Instance: - # Type: AWS::EC2::Instance - # DependsOn: - # - MyInstanceSecurityGroup - # Properties: - # ImageId: ami-05fb0b8c1424f266b - # InstanceType: t2.micro - # SecurityGroupIds: - # - !GetAtt MyInstanceSecurityGroup.GroupId - # KeyName: general_strides - - MyDBSecurityGroup: - Type: AWS::EC2::SecurityGroup - DependsOn: - - MyInstanceSecurityGroup - Properties: - GroupDescription: Allow access to PostgreSQL - VpcId: vpc-0e2c306eb7a371817 - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 5432 - ToPort: 5432 - SourceSecurityGroupId: !Ref MyInstanceSecurityGroup - - MyCacheSecurityGroup: - Type: AWS::EC2::SecurityGroup - DependsOn: MyInstanceSecurityGroup - Properties: - GroupDescription: Allow access to Redis - VpcId: vpc-0e2c306eb7a371817 - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 6379 - ToPort: 6379 - SourceSecurityGroupId: !Ref MyInstanceSecurityGroup - - MyInstanceSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: Security group for EC2 instance - VpcId: vpc-0e2c306eb7a371817 - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 22 - ToPort: 22 - CidrIp: 107.200.64.20/32 - - IpProtocol: tcp - FromPort: 80 - ToPort: 80 - CidrIp: 0.0.0.0/0 - -Outputs: - RDSInstanceEndpoint: - Description: Endpoint of the RDS instance - Value: !GetAtt MyDBInstance.Endpoint.Address - - RDSProxyEndpoint: - Description: Endpoint of the RDS Proxy - Value: !GetAtt MyDBProxy.Endpoint - - RedisEndpoint: - Description: Endpoint of the Redis ElastiCache instance - Value: !GetAtt MyElastiCacheRedis.RedisEndpoint.Address - - EC2InstancePublicIP: - Description: Public IP of the EC2 instance - Value: !GetAtt MyEC2Instance.PublicIp