Deploying Python Dash and R Shiny Apps with ShinyProxy, Docker, Amazon EC2 and Cognito

In a previous post, we documented the steps for deploying shiny applications or interactive documents through shiny-server, rstudio-server, and Amazon EC2. This post documents the steps for deploying apps using Docker, ShinyProxy, Amazon EC2, and Amazon Cognito.

Step 1: AWS Set Up

If you are working for an organization that uses AWS, the chances are that your IT department may already have compute instances or protocols for requesting cloud resources. In that case, seek approval for and obtain the following:

  • An IAM user or other other IAM entities with a minimum permissions set that allows for the provisioning of EC2 instances, ECR repositories, and Cognito user pools.

  • To set up the server, we would need to connect to EC2 via a Secure Shell (SSH) using a Command Line Interface (CLI), and so we need to obtain the Amazon EC2 .pem key pair.

The subsequent setup steps will vary depending on whether you’re using your organization’s EC2 instance or setting up your own. For this post, however, we will create our own Amazon Web Services (AWS) account, IAM entities and provision AWS resources. The first step is to register for an AWS account, which is free of charge.

AWS Command Line Interface

The AWS command line interface is a tool for managing AWS services from the command line. Follow the installation instruction here for your operating system. The AWS CLI will be used for the following:

IAM Administrator User

When we first create an Amazon Web Services (AWS) account, a root user with complete access to all AWS services is established. However, it is not recommended to use the root user for everyday tasks. Instead, we should create an IAM user with administrator access to manage routine activities such as user creation and resource provisioning.

To create such a user, follow the official documentation here. Ensure that AWS Management Console access is enabled:

From the administrator user’s console or via the AWS CLI, we can create further IAM entities—such as project-specific IAM users, IAM roles, and IAM policies—that can be used to securely manage and deploy AWS resources.

For simplicity, though it violates the principle of least privilege, all subsequent resources can be provisioned using an administrator-level user. However, it’s important to remain vigilant about IAM and resource access management best practices, particularly in enterprise environments where security and access control are critical.

Key Pair

To authenticate when connecting to Amazon EC2 instances, a set of credentials called a key pair is required, consisting of a public key and a private key. In the EC2 console, navigate to Network & Security -> Key Pairs and create a new key pair:

The different types of key pairs correspond to the underlying signature algorithms. For this setup, choose ED25519. After provisioning the key pair, download the private key file (.pem) to our local machine; this .pem file will be used to connect to the EC2 instance via SSH.

AWS CloudFormation

AWS CloudFormation is a service that allows us to provision and configure AWS resources (like Amazon EC2 instances, S3 buckets, or Amazon RDS DB instances) using a template that describes them. This means that we no longer have to individually deploy, configure, and terminate AWS services.

With AWS CloudFormation, we describe the resources and their properties in a template file (either written in JSON or YAML) and create a stack, which is a collection of resources. Create a shinyproxy-template.yaml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template to create a VPC with a single subnet, custom security group, and an EC2 instance.

Parameters:
  WhiteListIP:
    Description: The IP address or range of IPs that will be allowed to SSH into the instance (e.g., IPV4/32)
    Type: String
    AllowedPattern: ^([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?$
    ConstraintDescription: Must be a valid IP CIDR range or a single IP address.

  ImageId:
    Description: AMI ID for the EC2 instance (Defaults to Amazon Linux 2023 AMI)
    Type: String
    Default: ami-066784287e358dad1  # Amazon Linux 2023 AMI 
    
  DeviceName:
    Description: Device name for the root volume on the EC2 instances
    Type: String
    Default: '/dev/xvda' 

  InstanceType:
    Description: The EC2 instance type.
    Type: String
    Default: 't3.micro'
    AllowedValues: 
      - t2.micro
      - t2.small
      - t2.medium
      - t2.large
      - t2.xlarge
      - t2.2xlarge
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
    ConstraintDescription: Must be a valid EC2 instance type.

  KeyName:
    Description: The name of the EC2 Key Pair to allow SSH access to the instance.
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: Must be the name of an existing EC2 Key Pair.

  VolumeSize:
    Description: The size of the EBS volume in GiB, defaults to 30 GiB.
    Type: Number
    Default: 30
    MinValue: 8
    MaxValue: 1024
    ConstraintDescription: Must be between 8 and 1024 GiB.

  DeleteOnTermination:
    Description: Whether to delete the EBS volume when the instance is terminated.
    Type: String
    Default: 'true'

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.0.0.0/16

  PublicSubnetCIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.0.0.0/24

  UseElasticIP:
    Description: Set to 'true' to allocate and associate an Elastic IP with the instance, defaults to 'false'.
    Type: String
    Default: 'false'
    AllowedValues:
      - 'true'
      - 'false'

Conditions:
  CreateElasticIP: !Equals [!Ref UseElasticIP, 'true']

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-vpc

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties: 
      Tags: 
        - Key: Name
          Value: !Sub ${AWS::StackName}-igw

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: !Sub ${AWS::StackName}-rtb

  PublicRoute:
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnetCIDR
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select 
        - 0
        - !GetAZs ''
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-subnet

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      VpcId: !Ref VPC
      GroupDescription: Security group for ShinyProxy EC2 instances controlling inbound traffic
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - CidrIp: '0.0.0.0/0' # Shiny-server port
          IpProtocol: tcp
          FromPort: 3838
          ToPort: 3838
        - CidrIp: '0.0.0.0/0' # Shinyproxy port
          IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
        - CidrIp: '0.0.0.0/0' # Dash port
          IpProtocol: tcp
          FromPort: 8050
          ToPort: 8050
        - CidrIp: '0.0.0.0/0' # HTTP port
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
        - CidrIp: '0.0.0.0/0' # HTTPS port
          IpProtocol: tcp
          FromPort: 443
          ToPort: 443
        - CidrIp: !Ref WhiteListIP  # SSH access from the specified IP address or range
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-sg

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties: 
      InstanceType: !Ref InstanceType
      KeyName: !Ref KeyName
      ImageId: !Ref ImageId
      SecurityGroupIds:
        - !Ref SecurityGroup
      SubnetId: !Ref PublicSubnet
      BlockDeviceMappings:
        - DeviceName: !Ref DeviceName
          Ebs:
            VolumeType: gp3
            VolumeSize: !Ref VolumeSize
            DeleteOnTermination: !Ref DeleteOnTermination
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-instance
    DependsOn: VPCGatewayAttachment

  EIP:
    Condition: CreateElasticIP
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref EC2Instance
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-eip
    DependsOn: EC2Instance

The sub-sections below provide some additional details on the most important resources provisioned in this stack.

Mandatory Parameters

  1. WhiteListIP:
    • Description: The IP address in CIDR notation (e.g., Your-IPV4/32) to whitelist for SSH access to the EC2 instance. Whitelisting an IP address ensures that only those using the specified IP can initiate an SSH connection to the EC2 instance. This is a critical security measure to prevent unauthorized access attempts from unknown sources. It is essential to only allow trusted IPs to maintain a secure environment.
  2. KeyName:
    • Description: Name of an existing EC2 Key Pair to enable SSH access to the instances.

Parameters with Default Values

  1. ImageId:
    • Description: AMI ID for the EC2 instance. By default, it uses the Amazon Linux 2023 AMI, which is a general-purpose Linux image provided by AWS.
    • Default: ami-066784287e358dad1 (Amazon Linux 2023 AMI)
  2. DeviceName:
    • Description: Device name for the root volume on the EC2 instances. The default value is /dev/xvda, which is standard for Amazon Linux 2023 AMI.
  3. InstanceType:
    • Description: The type of EC2 instance to be provisioned for ShinyProxy.
    • Default: t3.micro
    • Allowed Values: t2.micro, t2.small, t2.medium, t2.large, t2.xlarge, t2.2xlarge, t3.micro, t3.small, t3.medium, t3.large, t3.xlarge, t3.2xlarge
  4. VolumeSize:
    • Description: The size of the EBS volume in GiB, defaults to 30 GiB. The range is between 8 to 1024 GiB, which is configurable depending on storage needs.
  5. VpcCIDR:
    • Description: The IP range (CIDR notation) for this VPC.
    • Default: 10.0.0.0/16
  6. PublicSubnetCIDR:
    • Description: The IP range (CIDR notation) for the public subnet in the first Availability Zone.
    • Default: 10.0.0.0/24
  7. UseElasticIP:
    • Description: Set to ‘true’ to allocate and associate an Elastic IP with the instance. This ensures a static, publicly accessible IP address, which is especially useful if the application needs to be consistently reachable.
    • Default: false
    • Allowed Values: true, false

VPC and Subnet Configuration

The Virtual Private Cloud (VPC) serves as an isolated virtual network, providing a controlled environment for our AWS resource deployments. Subnets further split this VPC based on availability zones, allowing for public and private resource segregation.

ResourceDescription
VPCThe main virtual private cloud resource.
PublicSubnetA subnet with resources accessible from the internet.

Internet Gateway

This component ensures that resources within the VPC can connect to the internet.

ResourceDescription
InternetGatewayEnables internet access for the VPC.

Route Tables

Route tables contain a set of rules, called routes, that determine where network traffic is directed. In this template, a route table is defined for the public subnet.

One key route is PublicRoute, which is set up to redirect all outbound traffic to IP (0.0.0.0/0), so the EC2 instance can access the broader internet via the Internet Gateway. This ensures that tools like nginx and shinyproxy can be downloaded from the internet.

ResourceDescription
PublicRouteTableRoute table for the public subnet.

EC2 Configuration: ShinyProxy Instance

The Elastic Compute Cloud (EC2) is a service offering scalable computing capacity in AWS. This template provisions a single EC2 instance placed in the public subnet.

ResourceDescription
EC2InstanceThe primary EC2 instance, hosting ShinyProxy.

Security Group Configuration

Security Groups act as virtual firewalls, controlling inbound and outbound traffic to resources.

ResourceDescription
SecurityGroupControls traffic for the EC2 instance, including HTTP, HTTPS, SSH, and ports for ShinyProxy and other applications.

Elastic IP Configuration

The Elastic IP (EIP) provides a static IP address for the EC2Instance. This is crucial for maintaining consistent access and is particularly important for network configurations where a stable IP is necessary.

ResourceDescription
EIPA static IP address assigned to the EC2 instance for consistent external access.

Note on dependencies: The EIP is associated with the EC2Instance and has a DependsOn attribute to ensure proper creation order. This is important to align with the VPC and Internet Gateway setup in the template, adhering to AWS best practices.

Before moving on, we can validate the template for syntax issues using the AWS CLI:

$ aws cloudformation validate-template --profile your-profile --template-body file:///path/to/shinyproxy-template.yaml

Stack Creation

We are now ready to create the stack from the CloudFormation console.

The CloudFormation template file can be either uploaded directly in the creation interface or stored in an S3 bucket and referenced during stack creation.

The two required parameters in the template are:

  • KeyName: The name of the key pair to use for SSH access to the EC2 instance. This is the name of the .pem file downloaded earlier.

  • WhiteListIP: The IP address range that can access the EC2 instance via SSH. This is the IP address of the machine being used to SSH into the EC2 instance. To find the IP address, search “what is my IP address” on Google. For example, if the IPv4 address is 192.168.1.1, enter 192.168.1.1/32 as the value for WhiteListIP.

Everything else can either be left as default or adjusted to meet specific requirements. If an error occurs during stack creation, check the event log for more information.

Step 2: Connecting to Amazon EC2

To connect to our EC2 instance via SSH, we will use the terminal (for windows, use PuTTY). Open the terminal, navigate to the location of our .pem key:

# Set permission for read only by the owner 
$ cd path_to_pem_key && chmod 400 my_key.pem

In order to SSH into our EC2 instance:

# Ubuntu
$ ssh -i my_key.pem ubuntu@elastic-ip-address
# Amazon Linux 2023
$ ssh -i my_key.pem ec2-user@elastic-ip-address
  • The elastic IP address for the EC2 instance can be retreived from “Elastic IPs” tab under “Network & Security” from the side menu of the EC2 console.

  • The default username for Amazon Linux 2023 is ec2-user. For Ubuntu, the default username is ubuntu.

To disconnect from our instances:

$ exit

Important: From this point on, the command line syntax and tools used to download/install packages will differ depending on the AMI we selected above. In the template above, we chose an AMI that uses Amazon Linux 2023, but Ubuntu is also a popular choice. Therefore, we will include syntax for both of these operating systems.

If you are a Visual Studio Code user, you could follow this Youtube video or Microsoft’s official documentation to set up remote SSH, which allows us to open a remote folder on any remote machine, virtual machine, or container with a running SSH server. Compared to using the terminal, this has the added benefit of allowing us to use a GUI to view files and directory trees on the EC2 instance.

Step 3: Docker

Installation

Install docker on the EC2 instance.

Ubuntu

Based on the instructions in the official Docker documentation:

# Update command
$ sudo apt-get update
# Install packages to allow apt to use a repository over HTTPS
$ sudo apt-get install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
# Add Docker’s official GPG key
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Set up the docker repository
$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update
$ sudo apt-get update
# Install
$ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Verify installation
$ sudo docker run hello-world

The difference between apt-get and apt is that the former is an older command with more options while apt is a newer, more user-friendly command with fewer options.

Amazon Linux 2023

# Update
$ sudo yum update -y
$ sudo yum install -y docker
# Check status
$ sudo systemctl start docker
$ sudo systemctl status docker

Docker Startup Options

According to the official documentation, ShinyProxy needs to connect to the docker daemon to spin up the containers for the applications. By default, ShinyProxy will do so on port 2375 of the docker host (our EC2 instance). In order to allow for connections on port 2375, the docker startup options must be modified.

Ubuntu

$ sudo mkdir /etc/systemd/system/docker.service.d
$ sudo touch /etc/systemd/system/docker.service.d/override.conf
$ sudo nano /etc/systemd/system/docker.service.d/override.conf

Amazon Linux 2023

# Edit docker.service.d
$ sudo systemctl edit docker

For both operating systems, add the following content to the docker startup options:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H unix:// -D -H tcp://127.0.0.1:2375

Restart docker:

# Ubuntu
$ sudo systemctl daemon-reload
# Amazon Linux 2023
$ sudo systemctl restart docker

Useful Commands for Docker

Ubuntu

$ sudo service docker status
$ sudo service docker start
$ sudo service docker stop
$ sudo service docker restart
$ sudo docker version
# List docker images
$ sudo docker image ls
# Remove docker images with '-f' force remove option
$ sudo docker image rm -f image_id

Amazon Linux 2023

$ sudo systemctl enable docker.service
$ sudo systemctl status docker.service
$ sudo systemctl start docker.service
$ sudo systemctl stop docker.service
$ sudo systemctl restart docker.service
$ sudo docker version
# List docker images
$ sudo docker image ls
# Remove docker images with '-f' force remove option
$ sudo docker image rm -f image_id

Step 4: Nginx

We will utilize nginx to set up a reverse proxy for our application. As detailed in my earlier post, the motivation behind this architectural decision is multifaceted. Fundamentally, off-loading SSL encryption tasks to a separate entity, like nginx, ensures efficient resource utilization and optimized performance. This setup is particularly beneficial for a tool like ShinyProxy, where the primary focus is on serving content or running applications.

Installation

Ubuntu

$ sudo apt-get install -y nginx
$ sudo nginx -v

Amazon Linux 2023

$ sudo yum install -y nginx
$ sudo nginx -v

Useful Commands for Nginx

Ubuntu

$ sudo service nginx status
$ sudo service nginx start
$ sudo service nginx stop
$ sudo service nginx restart

Amazon Linux 2023

$ sudo systemctl status nginx
$ sudo systemctl start nginx
$ sudo systemctl stop nginx
$ sudo systemctl restart nginx

Configurations

The nginx configuration files are located in the etc (system configuration files) directory:

$ cd /etc/nginx
$ ls

The results of ls may differ depending on the AMI (and thus the operating system) that we used.

Ubuntu

With Ubuntu, the default installation of nginx would create a sites-avalable and a sites-enabled directory. Navigate to the /etc/nginx directory, we should see at least the following sub-directories: conf.d, sites-enabled, nginx.conf, sites-available (if not, we can create them). In order to modularize the configuration files, we will create project-based configuration files in the sites-available directory and create a symbolic link in the sites-enabled directory to activate them. Ensure that the following directive is included in the nginx.conf file:

$ sudo service nginx stop
$ sudo nano /etc/nginx/nginx.conf
http {
  
    ... 
    
    ##
    # Virtual Host Configs
    ##
    
    # Ensure that these directives are included
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Create a new configuration file specifically for ShinyProxy:

$ sudo nano /etc/nginx/sites-available/shinyproxy.conf

The ShinyProxy official documentation has an example configuration for nginx. With some additional enhancements, copy and paste the following block of directives to the shinyproxy.conf file:

server {
  listen                80;
  # Enter ourdomain.com or subdomain.ourdomain.com
  server_name           ourdomain.com;
  rewrite     ^(.*)     https://$server_name$1 permanent;
}

server {
  listen                443 ssl;
  # Enter ourdomain.com or subdomain.ourdomain.com
  server_name           ourdomain.com;
  access_log            /var/log/nginx/shinyproxy.access.log;
  error_log             /var/log/nginx/shinyproxy.error.log error;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  # Diffie-Hellman key
  ssl_dhparam /etc/ssl/certs/dhparam.pem;

  # OCSP stapling
  ssl_stapling on;
  ssl_stapling_verify on;

  # Enter the paths to our ssl certificate and key file created in the previous subsection
  ssl_certificate       /etc/letsencrypt/live/ourdomain.com/fullchain.pem;
  ssl_certificate_key   /etc/letsencrypt/live/ourdomain.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/ourdomain.com/chain.pem;

  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;  # About 40000 sessions
  ssl_session_tickets off;

  location / {
      proxy_pass          http://127.0.0.1:8080/;

      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_read_timeout 600s;

      proxy_redirect    off;
      proxy_set_header  Host              $http_host;
      proxy_set_header  X-Real-IP         $remote_addr;
      proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header  X-Forwarded-Proto $scheme;
    }

}

In the configuration file above, substitute all ourdomain.com with our custom domain and modify the paths to the following directives:

  • ssl_certificate and ssl_certificate_key

  • ssl_trusted_certificate

  • ssl_dhparam

Next, we need to create a shortcut (symbolic link) inside the sites-enabled directory. The reason for this is that nginx does not look at sites-available but only the sites-enabled directory in the /etc/nginx/nginx.conf configuration file. We create the .conf files inside sites-available and create a shortcut inside sites-enabled to access it. One benefit of this is that, to temporarily deactivate access to ShinyProxy, we only have to delete the shortcut but not the actual configuration file in sites-available:

$ sudo ln -s /etc/nginx/sites-available/shinyproxy.conf /etc/nginx/sites-enabled/

Important: By default, there will be default configuration files located in the sites-available and sites-enabled directories, which we must remove:

$ sudo rm /etc/nginx/sites-enabled/default && sudo rm /etc/nginx/sites-available/default

Amazon Linux 2023

On RedHat, CentOS, and Fedora, the default nginx installation does not create directories such as sites-available and sites-enabled. For these operating systems, the standard directory to store configuration files (ending in .conf) is /etc/nginx/conf.d/*.conf.

Similarly, within the /etc/nginx/nginx.conf configuration file, it’s essential to include the directive include /etc/nginx/conf.d/*.conf; within the http block. This ensures that nginx recognizes and incorporates any .conf files located in the /etc/nginx/conf.d directory. By default, this directive is included in the http block, but it is always good to double-check:

$ sudo systemctl stop nginx
$ sudo nano /etc/nginx/nginx.conf
http {
  
    ... 
    
    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;
}

Write the same block of directives in the shinyproxy.conf file:

$ sudo nano /etc/nginx/conf.d/shinyproxy.conf
server {
  listen                80;
        ...
}

server {
  listen                443 ssl;
  
          ...
  
   location / {
       proxy_pass          http://127.0.0.1:8080/;
     
          ...
}

Again, make sure to substitute all ourdomain.com with our custom domain and modify the ssl_ paths. Remove the default.d configuration directory:

$ sudo rm -r /etc/nginx/default.d

Testing Syntax

For both operating systems, to test if the configuration files are syntactically correct, run the following:

$ sudo nginx -t

This should output the results below if the configuration test has passed:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

We are now ready to restart nginx:

# Ubuntu
$ sudo service nginx restart
# Amazon Linux 2023
$ sudo systemctl restart nginx

If restart fails, try deleting all running processes on port 80 first:

$ sudo lsof -t -i :80 | xargs sudo kill

Configurations Breakdown (Optional)

When nginx proxies a request, it performs the following steps:

  1. Forwards the Request: When a client attempts to access a website or server (e.g., an application on an EC2 instance), nginx intercepts the request and forwards it to the appropriate server hosting the website or application.
  2. Retrieves the Response: After forwarding the request, nginx waits for the server’s response, which contains the requested information, such as a webpage or application data.
  3. Delivers the Response Back to the Client: Once nginx receives the server’s response, it sends this information back to the client’s device, allowing interaction with the website or application.

Here’s a breakdown of the configuration file details:

Section/DirectiveDescription
server (first block)Listens on port 80, the standard port for HTTP, and is responsible for redirecting all HTTP traffic to HTTPS for enhanced security.
listen 80Configures the server to listen for incoming connections on port 80 (HTTP).
server_nameSpecifies the domain name for this server block, set here as ourdomain.com.
rewriteImplements a 301 permanent redirect, directing all HTTP traffic to HTTPS on the same domain.
server (second block)Listens on port 443, the standard port for HTTPS, handling secure connections.
listen 443Configures the server to listen for incoming connections on port 443 (HTTPS).
access_log and error_logDefine the locations for nginx’s access and error logs.
ssl_protocolsSpecifies the allowed TLS protocols, configured here to permit TLS versions 1.0, 1.1, and 1.2.
ssl_certificate, ssl_certificate_key, and ssl_trusted_certificateProvide the paths to the SSL certificate, private key, and trusted CA certificates.
ssl_stapling and ssl_stapling_verifyEnable OCSP stapling, a method for verifying the validity of SSL certificates.
ssl_dhparamSpecifies the path to the Diffie-Hellman key exchange file for enhanced security.
location /Defines how to handle requests for the root URL (/), configured to proxy these requests to another application running on port 8080.
proxy_passDirects requests to the specified proxied server. More details are available here.
proxy_http_versionSets the HTTP protocol version for proxying. More details here.
proxy_set_header (multiple)Sets header values that will be passed to the proxied server, crucial for WebSocket support and conveying the original client information. Note: The proxy_set_header X-Forwarded-Proto $scheme is important to ensure that ShinyProxy correctly handles redirects based on HTTP or HTTPS requests.
proxy_redirectSet to off, ensuring that nginx does not modify the “Location” and “Refresh” header fields in the proxied server’s response.
proxy_read_timeoutDefines a timeout for reading a response from the proxied server, set to 600 seconds.

Step 5: Shinyproxy

ShinyProxy is built using mature and robust Java technology, so a Java 8 (or higher) runtime environment is required to run ShinyProxy. For the latest version, Java 17 is required.

Installation

Ubuntu

Java installation:

# Shinyproxy requires Java 17 (or higher)
$ sudo apt-get -y update
$ sudo apt-get -yq install \
    openjdk-17-jdk \
    openjdk-17-jre 
# Check version
$ java -version

Download the most recent version shinyproxy from the official donwload page based on the platform. For Ubuntu (Debian Linux), download the deb file:

# Shinyproxy latest at the time of writing this post
$ export SHINYPROXY_VERSION="3.1.1"
# Download and install deb file
$ wget https://www.shinyproxy.io/downloads/shinyproxy_${SHINYPROXY_VERSION}_amd64.deb
$ sudo apt install ./shinyproxy_${SHINYPROXY_VERSION}_amd64.deb
$ rm shinyproxy_${SHINYPROXY_VERSION}_amd64.deb

Amazon Linux 2023

Java installation instructions:

# Install amazon corretto 17 headless
$ sudo yum install java-17-amazon-corretto-headless -y
$ java -version

Download the most recent version shinyproxy from the official donwload page based on the platform. For Amazon Linux 2023, download the rpm file:

$ export SHINYPROXY_VERSION="3.1.1"
# Downloads RPM package file to current directory
$ sudo wget https://www.shinyproxy.io/downloads/shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm
$ sudo yum localinstall -y ./shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm
$ sudo rm ./shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm
# Check installation
$ sudo systemctl status shinyproxy

If the installation fails due to missing dependencies, try:

$ sudo rpm -i --nodeps shinyproxy_${SHINYPROXY_VERSION}_x86_64.rpm

Useful Commands for ShinyProxy

Ubuntu

$ sudo service shinyproxy status
$ sudo service shinyproxy start
$ sudo service shinyproxy stop
$ sudo service shinyproxy restart

Amazon Linux 2023

$ sudo systemctl status shinyproxy
$ sudo systemctl start shinyproxy
$ sudo systemctl stop shinyproxy
$ sudo systemctl restart shinyproxy

Configurations

ShinyProxy is configured using the application.yml file. Regardless of whether we’re using Ubuntu or Amazon Linux 2023, we should find this configuration file in the same location:

$ sudo nano /etc/shinyproxy/application.yml

The file below is the default configuration created by installing ShinyProxy:

proxy:
  title: Open Analytics Shiny Proxy
  logo-url: https://www.openanalytics.eu/shinyproxy/logo.png
  landing-page: /
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 8080
  authentication: simple
  admin-groups: scientists
  # Example: 'simple' authentication configuration
  users:
    - name: jack
      password: password
      groups: scientists
    - name: jeff
      password: password
      groups: mathematicians
    port-range-start: 20000
  specs:
    - id: 01_hello
      display-name: Hello Application
      description: Application which demonstrates the basics of a Shiny app
      container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
      container-image: openanalytics/shinyproxy-demo
      access-groups: [ scientists, mathematicians ]
    - id: 06_tabsets
      container-cmd: [ "R", "-e", "shinyproxy::run_06_tabsets()" ]
      container-image: openanalytics/shinyproxy-demo
      access-groups: scientists

logging:
  file:
    name: shinyproxy.log

For an in-depth explanation of each directive in the configuration file, refer to the official ShinyProxy documentation. Here are some key points to understand for a basic setup:

  • Port: We have set the port directive to 8080, which corresponds to the port we established during our EC2 instance creation.

  • Authentication: By default, ShinyProxy allows us to specify users and their access levels via the authentication directive. While this is a straightforward approach, you might want to consider more secure authentication methods. Thankfully, ShinyProxy is compatible with various authentication protocols such as the OpenID Connect. We will set up a more secure authentication method in the subsequent section.

  • Docker: With the docker directive, we can define the url and port to communicate with the docker daemon.

  • Template Groups: The template-groups directive allows us to categorize apps in the template into groups, allowing different user groups to access different applications.

  • Apps: Every application that ShinyProxy serves requires its own configuration under the specs section. We will delve into this after containerizing our Shiny and Dash applications in the subsequent section.

Test ShinyProxy

To ensure ShinyProxy is working as expected, we can pull the demo application image from the OpenAnalytics repository:

$ sudo docker pull openanalytics/shinyproxy-demo

Restart ShinyProxy:

# Ubuntu
$ sudo service shinyproxy restart
# Amazon Linux 2023
$ sudo systemctl restart shinyproxy

We should now be able to access the ShinyProxy login page at the following URL: https://ourdomain or https://subdomain.ourdomain. The login credentials are contained in the sample configuration file above.

Updating Configuration (Important)

# Ubuntu
$ sudo service shinyproxy stop
# Amazon Linux 2023
$ sudo systemctl stop shinyproxy

Forwarded Headers & Secure Cookies

According to the ShinyProxy documentation, when ShinyProxy is accessed through a reverse proxy that uses HTTPS, the direct connection to ShinyProxy is often over HTTP. This discrepancy can cause ShinyProxy to generate incorrect URLs, especially redirect URIs, using the “http” scheme instead of “https”. To address this, reverse proxies use the X-Forwarded-* headers (X-Forwarded-For and X-Forwarded-Proto) to specify the original protocol used by the client to access ShinyProxy.

In the application.yml configuration file, we need to set the forward-headers-strategy to native, so that ShinyProxy uses these headers to generate the correct URLs with the https scheme. In addition, we can also set secure-cookies to true to set the secure flag on all session cookies.

$ sudo nano /etc/shinyproxy/application.yml

Add the following to the configuration yaml file at the same level as the proxy entry:

proxy:
  title: Open Analytics Shiny Proxy
  [...] # Other proxy settings

server:
  forward-headers-strategy: native
  secure-cookies: true

Restrict ShinyProxy to Only Bind to Local Host

Because nginx will pass requests to port 8080 on the loopback interface (127.0.0.1) based on the proxy_pass directive in the nginx configuration file. The ShinyProxy documentation recommends that we restrict ShinyProxy to bind only on 127.0.0.1 (and not to the default 0.0.0.0). We can set the bind-address in the application.yml configuration file:

proxy:
  [...] # Other proxy settings
  bind-address: 127.0.0.1 # Add this under the proxy entry

Container Logging

We can add the proxy.container-log-path directive to the application.yml configuration file to specify the path where the container logs will be written. This is useful for debugging purposes:

proxy:
  [...] # Other proxy settings
  bind-address: 127.0.0.1
  container-log-path: /var/log/shinyproxy # Add this under the proxy entry

Once enabled, ShinyProxy will create log files with the following naming conventions:

<specId>_<proxyId>_<startupTime>_stdout.log
<specId>_<proxyId>_<startupTime>_stderr.log

For more details, see the ShinyProxy documentation.

Updated Configuration

The updated default configuration should look like this:

proxy:
  title: Open Analytics Shiny Proxy
  logo-url: https://www.openanalytics.eu/shinyproxy/logo.png
  landing-page: /
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 8080
  bind-address: 127.0.0.1 # Added the bind-address directive
  container-log-path: /var/log/shinyproxy # Added the container-log-path directive
  docker:
    port-range-start: 20000
  
  authentication: simple
  admin-groups: scientists
  users:
    - name: jack
      password: password
      groups: scientists
    - name: jeff
      password: password
      groups: mathematicians
      
  specs:
    - id: 01_hello
      display-name: Hello Application
      description: Application which demonstrates the basics of a Shiny app
      container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
      container-image: openanalytics/shinyproxy-demo
      access-groups: [ scientists, mathematicians ]
    - id: 06_tabsets
      container-cmd: [ "R", "-e", "shinyproxy::run_06_tabsets()" ]
      container-image: openanalytics/shinyproxy-demo
      access-groups: scientists
      
server:
  forward-headers-strategy: native # Added the forward-headers-strategy directive
  secure-cookies: true # Added the secure-cookies directive

logging:
  file:
    name: shinyproxy.log

Upon restart, Shinyproxy should now be accessible at https://ourdomain.com or https://subdomain.ourdomain.com:

# Ubuntu
$ sudo service shinyproxy restart
# Amazon Linux 2023
$ sudo systemctl restart shinyproxy

Step 6: Containerize Applications

To build images for our applications, we have two primary options:

  1. Using the EC2 Host: We can directly utilize the compute resources of our EC2 host. For small applications, this approach is straightforward and sufficient. However, for more complex applications, this may not be optimal due to the differing resource requirements during build-time and run-time. Additionally, tying up our EC2 host with the build process could lead to memory issues.

  2. Local Machine Builds: Alternatively, we can use our local machine (or any other machine that supports Docker) to build the images. Once built, these images can be pushed to the Elastic Container Registry (ECR) and then pulled to our EC2 host for deployment. This option offers greater flexibility, allowing us to leverage the resources of any machine for the build process.

The following subsections assume that docker is installed on both the EC2 host and our local machine. Additionally, both the machine used for building images and the EC2 instance should have the AWS Command Line Interface configured.

We will illustrate the deployment of two simple applications: one built with R Shiny and the other with Python Dash. Both applications will use a bash script for building the images.

R Shiny

On the machine that will be used for building the images, create a directory for the Shiny application:

.
└── shiny_app
    ├── Dockerfile.shiny
    └── app.R

The app.R script contains the code for our Shiny application. This simple application achieves the following:

  • Simulates predictors \(x_1\) through \(x_6\) from i.i.d. normal distributions with mean 0 and standard deviation 1.

  • The response variable \(y\) is deterministically calculated using a linear model with predefined coefficients, utilizing only predictors \(x_1\) through \(x_3\) (i.e., the coefficients for predictors \(x_4\) through \(x_6\) are zero).

  • The user can select the regularization parameter \(\lambda\) for the Lasso and Ridge regression models using a slider.

  • The application plots the coefficients of the Lasso and Ridge regression models to illustrate the effects of regularization on the statistical significance of model coefficients.

library(bs4Dash)
library(shiny)
library(glmnet)
library(plotly)

# Simulation function -----------------------------------------------------

simulate_data <- function(n = 1000) {
  # Predictor variables used to simulate the response variable
  significant_predictors <- lapply(
    c("x1", "x2", "x3"),
    function(x) rnorm(n = n, mean = 0, sd = 1)
  )

  # Predictor variables not used to simulate the response variable
  insignificant_predictors <- lapply(
    c("x4", "x5", "x6"),
    function(x) rnorm(n = n, mean = 0, sd = 1)
  )

  # True parameter values
  b_1 <- 12.34
  b_2 <- 23.45
  b_3 <- 2.45

  # Simulate the response variable
  intercept <- 20
  y <- intercept + b_1 * significant_predictors[[1]] + 
      b_2 * significant_predictors[[2]] + b_3 * significant_predictors[[3]]

  # Standardize the response variable
  y <- (y - mean(y)) / sd(y)
  
  data <- data.frame(
    y = y,
    x1 = significant_predictors[[1]],
    x2 = significant_predictors[[2]],
    x3 = significant_predictors[[3]],
    x4 = insignificant_predictors[[1]],
    x5 = insignificant_predictors[[2]],
    x6 = insignificant_predictors[[3]]
  )

  return(data)
}

# Train models ------------------------------------------------------------

train_models <- function(data, regularization_strength) {
  predictors <- c("x1", "x2", "x3", "x4", "x5", "x6")

  # Train models
  lasso <- glmnet(
    x = as.matrix(data[, predictors]),
    y = data$y,
    alpha = 1,
    lambda = regularization_strength
  )

  ridge <- glmnet(
    x = as.matrix(data[, predictors]),
    y = data$y,
    alpha = 0,
    lambda = regularization_strength
  )

  # Coefficients
  coefs <- data.frame(
    predictor = rep(predictors, times = 2),
    beta = c(coef(ridge)[predictors, ], coef(lasso)[predictors, ]),
    model = rep(c("Ridge", "Lasso"), each = length(predictors))
  )

  return(coefs)
}

# UI ----------------------------------------------------------------------

ui <- bs4DashPage(
  title = "Shiny Application",
  header = bs4DashNavbar(disable = TRUE),
  sidebar = bs4DashSidebar(disable = TRUE),
  body = bs4DashBody(
    bs4Card(
      sliderInput(
        inputId = "regularization_strength",
        label = "Use the slider to change the regularization strength for Lasso and Ridge regression models:",
        min = 0,
        max = 1,
        value = 0.2
      ),
      title = "Regularization Strength",
      width = 12
    ),
    bs4Card(
      plotlyOutput("box_plot"),
      width = 12
    )
  ),
  controlbar = dashboardControlbar(disable = TRUE)
)

# Server ------------------------------------------------------------------

server <- function(input, output) {
  # Simulate 200 data sets, each with 200 observations
  data <- reactive({
    replicate(200, simulate_data(n = 200), simplify = FALSE)
  })

  # Train models for each data set
  results <- reactive({
    models <- lapply(data(), train_models, regularization_strength = input$regularization_strength)
    # Combine results (row-bind)
    do.call(rbind, models)
  })

  # Box plot
  output$box_plot <- renderPlotly({
    plot_ly(
      data = results(),
      x = ~predictor,
      y = ~beta,
      color = ~model,
      type = "box"
    ) %>%
      layout(
        title = "Box plot",
        xaxis = list(title = "Predictor"),
        yaxis = list(title = "Coefficient")
      )
  })
}

shinyApp(ui, server)

The Dockerfile.shiny file packages the application code for deployment. A Dockerfile is a text file containing instructions to assemble an image by layer. This Dockerfile builds from the r-ver image, a Ubuntu-based Linux image with the a fixed version of R. For more Docker images for R, see the Rocker Project. The installation of R packages makes use of the install.r script, which is a part of littler package.

FROM r-base:4.3.2 

WORKDIR /shiny_app

COPY app.R ./

# System dependencies for R packages (bs4Dash & plotly)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    libcurl4-openssl-dev libssl-dev && \
    rm -rf /var/lib/apt/lists/*

# Install required R packages
RUN install.r --ncpus -1 shiny bs4Dash glmnet plotly

EXPOSE 3838

CMD ["R", "-q", "-e", "shiny::runApp('/shiny_app/app.R', port = 3838, host = '0.0.0.0')"]

Dash

Create a new directory for the Dash application:

.
├── dash_app
│   ├── Dockerfile.dash
│   ├── app.py
│   ├── entrypoint.py
│   └── requirements.txt

The app.py file contains the Dash application code, which is a sentiment analysis app. The app allows users to enter text and analyze the sentiment of the text. The sentiment analysis is performed using the TextBlob library, which is a Python library for processing textual data. The front-end is built with Dash, a Python framework for building web applications.

import os
import dash
from dash import html, dcc
from dash.dependencies import Input, Output
from textblob import TextBlob

app = dash.Dash(
    __name__,
    suppress_callback_exceptions=True,
    requests_pathname_prefix=os.environ['SHINYPROXY_PUBLIC_PATH'],
    routes_pathname_prefix=os.environ['SHINYPROXY_PUBLIC_PATH']
)
server = app.server

app.layout = html.Div([
    html.H1('Simple Sentiment Analysis App'),
    dcc.Textarea(
        id='text-input',
        value='',
        style={'width': '100%', 'height': 100},
        placeholder='Enter text here...'
    ),
    html.Button('Analyze', id='analyze-button'),
    html.Div(id='output-container')
])

@app.callback(
    Output('output-container', 'children'),
    [Input('analyze-button', 'n_clicks')],
    [dash.dependencies.State('text-input', 'value')]
)
def update_output(n_clicks, input_value):
    if n_clicks and input_value:
        analysis = TextBlob(input_value)
        sentiment_polarity = analysis.sentiment.polarity
        sentiment = 'Positive' if sentiment_polarity > 0 else 'Negative' if sentiment_polarity < 0 else 'Neutral'
        return f'Sentiment: {sentiment} (Polarity: {sentiment_polarity})'
    return 'Enter some text and click Analyze.'

if __name__ == '__main__':
  
    app.run_server(debug=True)

Dash requires knowledge of the path used to access the app. ShinyProxy makes this path available as an environment variable SHINYPROXY_PUBLIC_PATH. Next, the requirements.txt file lists the Python libraries required for the Dash app:

textblob==0.17.1
dash==2.14.1
plotly==5.18.0
gunicorn==21.2.0

We create an entrypoint.py file to start the Dash application using gunicorn, a Python WSGI HTTP Server for UNIX. This is one of the recommended ways to deploy Dash applications in production. For more information on the entrypoint script, see the gunicorn documentation on custom application.

import os
from typing import Dict, Any
from gunicorn.app.base import BaseApplication

# Importing the Dash app
from app import server as application

class StandaloneApplication(BaseApplication):
    """
    A standalone application to run a Dash app with Gunicorn. This 
    class is designed to configure and run a Dash application using 
    the Gunicorn WSGI HTTP server.

    Attributes
    ----------
    application : Any
        The Dash application instance to be served by Gunicorn.
    options : Dict[str, Any], optional
        A dictionary of configuration options for Gunicorn.
    """
    def __init__(self, app: Any, options: Dict[str, Any] = None) -> None:
        """
        Constructor for StandaloneApplication with a Dash app and options.

        Parameters
        ----------
        app : Any
            The Dash application instance to serve.
        options : Dict[str, Any], optional
            A dictionary of Gunicorn configuration options.
        """
        self.options = options or {}
        self.application = app
        super(StandaloneApplication, self).__init__()

    def load_config(self):
        """
        Load the configuration from the provided options. This method 
        extracts the relevant options from the provided dictionary and 
        sets them for the Gunicorn server.
        """
        config = {key: value for key, value in self.options.items() 
                  if key in self.cfg.settings and value is not None}
        for key, value in config.items():
            self.cfg.set(key.lower(), value)

    def load(self) -> Any:
        """
        Return the application to be served by Gunicorn. This method is 
        required by Gunicorn and is called to get the application instance.

        Returns
        -------
        Any
            The Dash application instance to be served.
        """
        return self.application
      
def main() -> int:
  
    # Retrieve the SHINYPROXY_PUBLIC_PATH from environment variable
    public_path = os.environ['SHINYPROXY_PUBLIC_PATH']

    options = {
        'bind': '0.0.0.0:8050',
        'workers': 4, 
        'env': {'SHINYPROXY_PUBLIC_PATH': public_path},
    }

    StandaloneApplication(application, options).run()
    
    return 0
    
if __name__ == '__main__':

    main()

The Dockerfile.dash in the dash_app directory would be similar to Dockerfile.shiny, but tailored to the Dash application:

FROM python:3.10.13-slim-bullseye

WORKDIR /dash_app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY app.py entrypoint.py ./

EXPOSE 8050

CMD ["python3", "entrypoint.py"]

Build and Push (Local Machine)

With the application source files created, we can build the images and push the images to ECR. Create a new private ECR repository by following the steps here:

Next, we will create a bash script build_and_push.sh to automate the following steps:

  • Build the docker images on the local machine

  • Obtain the AWS account ID and region and log in to the ECR registry

  • Push the docker images to ECR

On windows, we would need other means to run these steps. Note that the following script assumes that the AWS CLI is installed and configured on the local machine, and that the user has sudo privilege.

#!/bin/bash

# Function to prompt for input if arguments are not provided
prompt_for_input() {
    read -p "Enter the absolute path to the Dockerfile: " docker_image_path 
    read -p "Enter the custom image tag name: " image_tag
    read -p "Enter the ECR repository name: " ecr_repo
    read -p "Enter the AWS CLI credential profile name: " credential_profile
    read -p "Enter the target platform (e.g., linux/amd64): " target_platform
}

# Check if the required number of arguments is passed, otherwise prompt for input
if [ "$#" -eq 5 ]; then
    docker_image_path="$1"
    image_tag="$2"
    ecr_repo="$3"
    credential_profile="$4"
    target_platform="$5"
else
    prompt_for_input
fi

# Validate that all necessary inputs are provided
if [ -z "$docker_image_path" ] || [ -z "$image_tag" ] || [ -z "$ecr_repo" ] || [ -z "$credential_profile" ] || [ -z "$target_platform" ]; then
    echo "Error: Missing required inputs. Please provide the Dockerfile path, image tag, ECR repository name, AWS CLI credential profile name, and target platform."
    exit 1
fi

# Set the build context to the parent directory of the Dockerfile
build_context=$(dirname "$docker_image_path")

# AWS variables
account_id=$(aws sts get-caller-identity --profile "$credential_profile" --query Account --output text)
region=$(aws configure --profile "$credential_profile" get region)
image_name="$account_id.dkr.ecr.$region.amazonaws.com/$ecr_repo:$image_tag"

# Log in to the ECR registry
aws ecr get-login-password --profile "$credential_profile" --region "$region" | docker login --username AWS --password-stdin "$account_id.dkr.ecr.$region.amazonaws.com"

# Build and push the Docker image with the specified target platform
docker build --platform "$target_platform" -f "$docker_image_path" -t "$image_name" "$build_context"

if [ $? -ne 0 ]; then
    echo "Error: Docker build failed."
    exit 1
fi

docker push "$image_name"

if [ $? -ne 0 ]; then
    echo "Error: Docker push failed."
    exit 1
fi

echo "Docker image successfully built and pushed to ECR: $image_name"

The script can be run as follows:

$ bash build_and_push.sh /path/to/Dockerfile IMAGE-TAG ECR-REPO AWS-CREDENTIAL-PROFILE YOUR_TARGET_PLATFORM

Once the processes are completed, the images should be available in the ECR repository:

We can check that these images exist with:

$ aws ecr describe-images --profile AWS-CREDENTIAL-PROFILE --repository-name ECR-REPO

Pull and Run (EC2 Instance)

With our containerized applications built and pushed to ECR, we are now ready to pull the images and run them on our EC2 instance. Start by configuring the AWS CLI on the EC2 instance with credentials that have access to the ECR repository.

  • Amazon Linux 2023: The AWS CLI comes pre-installed.
  • Other Operating Systems: Follow the installation steps here.

Configure the AWS CLI:

# Follow the prompts to enter your credentials
$ aws configure --profile AWS-CREDENTIAL-PROFILE

Log in to Docker using the AWS credentials:

$ aws ecr get-login-password \
    --profile AWS-CREDENTIAL-PROFILE \
    --region AWS-REGION | \
  sudo docker login \
    --username AWS \
    --password-stdin \
    AWS-ACCOUNT-ID.dkr.ecr.AWS-REGION.amazonaws.com

Pull the images from ECR using their URIs, which follow this format: {AWS-ACCOUNT-ID}.dkr.ecr.{AWS-REGION}.amazonaws.com/{ECR-REPO}:{IMAGE-TAG}.

$ sudo docker pull IMAGE-URI

Verify that the images are available on the EC2 instance:

$ sudo docker image ls

Example output:

REPOSITORY                                                TAG         IMAGE ID       CREATED             SIZE
722696965592.dkr.ecr.us-east-1.amazonaws.com/shinyproxy   shiny_app   203637949297   5 minutes ago       1.05GB
722696965592.dkr.ecr.us-east-1.amazonaws.com/shinyproxy   dash_app    e69d02fa8bb4   6 minutes ago       316MB

This ensures the images are now available on the EC2 instance, ready to be deployed.

ShinyProxy Specs

Finally, update the ShinyProxy configuration file to use the new images:

# Ubuntu
$ sudo service shinyproxy stop
# Amazon Linux 2023
$ sudo systemctl stop shinyproxy

Open the ShinyProxy configuration file:

$ sudo nano /etc/shinyproxy/application.yml
proxy:
  title: ShinyProxy
  landing-page: /
  bind-address: 127.0.0.1
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 8080
  container-log-path: /var/log/shinyproxy # Added the container-log-path directive
  docker:
    port-range-start: 20000
    
  authentication: simple
  admin-groups: admin_users
  users:
    - name: admin_user
      password: password
      groups: admin_users
    - name: shiny_user
      password: password
      groups: shiny_users
    - name: dash_user
      password: password
      groups: dash_users

  specs:
    - id: shiny-app
      display-name: Ridge & Lasso App
      description: Demonstrate the effects of regularization on regularized linear models
      container-image: {AWS-ACCOUNT-ID}.dkr.ecr.{AWS-REGION}.amazonaws.com/{ECR-REPO}:{IMAGE-TAG}
      access-groups: [admin_users, shiny_users] # Accessible only by the admin and shiny_users groups
    - id: dash-app
      display-name: Sentiment Analysis App
      description: Analyze the sentiments of input text
      port: 8050
      container-image: {AWS-ACCOUNT-ID}.dkr.ecr.{AWS-REGION}.amazonaws.com/{ECR-REPO}:{IMAGE-TAG}
      target-path: "#{proxy.getRuntimeValue('SHINYPROXY_PUBLIC_PATH')}"
      access-groups: [admin_users, dash_users] # Accessible only by the admin and dash_users groups

server:
  forward-headers-strategy: native # Added the forward-headers-strategy directive
  secure-cookies: true # Added the secure-cookies directive

logging:
  file:
    name: shinyproxy.log

The user groups are set up as an example to ensure that shiny_user and dash_user have access only to the Shiny and Dash apps, respectively. The admin_user has access to all apps.

Remember to replace all placeholders with the appropriate values: {AWS-ACCOUNT-ID}.dkr.ecr.{AWS-REGION}.amazonaws.com/{ECR-REPO}:{IMAGE-TAG}. After updating, restart ShinyProxy.

# Ubuntu
$ sudo service shinyproxy start
# Amazon Linux 2023
$ sudo systemctl start shinyproxy

We can now access the ShinyProxy landing page by entering the domain or subdomain in the browser.

Step 7: OpenID Connect Authentication

ShinyProxy supports various authentication methods, including OpenID Connect (OIDC). OIDC allows users to authenticate via a third-party identity provider. In this section, we’ll set up OIDC authentication using Amazon Cognito, a managed service for authentication, authorization, and user management.

EntityDescription
UserThe individual accessing applications hosted on our EC2 instance, authenticated via Amazon Cognito.
ClientShinyProxy acts as the client, requesting tokens from Amazon Cognito to authenticate users and access resources.
Relying Party (RP)ShinyProxy also serves as the Relying Party (RP), outsourcing user authentication to Amazon Cognito, the identity provider.
OpenID Provider (OP) or Identity Provider (IDP)Amazon Cognito serves as the OpenID Provider (OP) or Identity Provider (IDP), handling authentication and issuing tokens (such as ID and access tokens) back to ShinyProxy.

ShinyProxy & OpenID Connect

The IDP manages users, including their credentials and other information. According to the ShinyProxy documentation, the login process with ShinyProxy is as follows:

  1. A user accesses the ShinyProxy server (e.g., https://ourdomain.com).
  2. ShinyProxy, acting as the client, redirects the user to the IDP (Amazon Cognito) over HTTPS (e.g., Cognito only allows HTTPS).
  3. The user logs in on the IDP’s website.
  4. The IDP redirects back to ShinyProxy.
  5. ShinyProxy requests tokens from the IDP over HTTP.
  6. The user is logged in to ShinyProxy.

This flow is called the Authorization Code Grant:

Authorization Code Grant
Illustration of the OAuth 2.0 Authorization Code Grant flow. Image source: WSO2 Identity Server Documentation.

The tokens returned by the IDP to ShinyProxy are:

  • ID Token: A short-lived token containing user information (claims or attributes), formatted as a JSON Web Token (JWT) and not refreshable.

  • Access Token: A short-lived token used to authorize user access to ShinyProxy, which can be refreshed using the refresh token.

  • Refresh Token: Allows refreshing the access token, ensuring continuous user access.

ShinyProxy continuously refreshes the access token to maintain user authorization. To configure ShinyProxy with Amazon Cognito, the following parameters are required:

  • Auth Endpoint URL: The URL where OIDC initiates authentication flows. ShinyProxy redirects unauthenticated users to this URL, e.g., at https://ourdomain.com.

  • Token Endpoint URL: The URL where tokens are retrieved or exchanged during the authentication process.

  • JSON Web Key Set (JWKS) URL: The URL where the IDP’s public certificates are found, used during the authentication process.

  • Client ID: A unique ID generated by the provider for our application, visible in the URL during authentication.

  • Client Secret: A secret generated by the provider for our application, which should be kept secure.

Amazon Cognito Setup

This CloudFormation template sets up the following resources:

  1. UserPool: A Cognito User Pool with enhanced security features like password policies and account recovery via verified email. It includes admin-controlled user creation and deletion protection.
  2. UserPoolClient: A client for the User Pool configured for OAuth 2.0 with allowed flows like authorization code (code) and scopes (openid, email). The client has token validity periods specified and supports secure interactions with ShinyProxy via callback and logout URLs.
  3. UserPoolDomain: A custom domain for the Cognito User Pool.
  4. AdminUsersGroup: A group named admin_users within the User Pool, intended for administrative users.
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template to create a Cognito User Pool, User Pool Client, and User Pool Domain.

Parameters:
  Domain:
    Type: String
    Description: The domain name for the Cognito User Pool Domain

Resources:
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub "${AWS::StackName}-user-pool"
      AccountRecoverySetting:
        RecoveryMechanisms:
          - Name: verified_email
            Priority: 1
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: True
      DeletionProtection: INACTIVE
      UserPoolAddOns:
        AdvancedSecurityMode: ENFORCED
      UsernameConfiguration:
        CaseSensitive: False
      AutoVerifiedAttributes:
        - email
      UserAttributeUpdateSettings:
        AttributesRequireVerificationBeforeUpdate:
          - email
      Policies:
        PasswordPolicy:
          MinimumLength: 12
          PasswordHistorySize: 3 # Number of previous passwords to restrict each user from reusing
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true
          TemporaryPasswordValidityDays: 3 # Number of days before an admin has to reset the temporary password

  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: !Sub "${AWS::StackName}-app-client"
      AllowedOAuthFlows:
        - code
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthScopes:
        - openid
        - email
      IdTokenValidity: 1  # 1 hour
      AccessTokenValidity: 1  # 1 hour
      RefreshTokenValidity: 24  # 24 hours
      TokenValidityUnits:
        IdToken: hours
        AccessToken: hours
        RefreshToken: hours
      CallbackURLs:
        - !Sub "https://${Domain}.com/login/oauth2/code/shinyproxy"
      LogoutURLs:
        - !Sub "https://${Domain}.com" # Ensure the logout URL is the same as the one in the ShinyProxy configuration
      ExplicitAuthFlows:
        - ALLOW_USER_SRP_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      GenerateSecret: true
      UserPoolId: !Ref UserPool

  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Ref Domain
      UserPoolId: !Ref UserPool

  AdminUsersGroup:
    Type: AWS::Cognito::UserPoolGroup
    Properties:
      GroupName: admin_users
      UserPoolId: !Ref UserPool
      Description: Administrator users for the application
      Precedence: 1

Outputs:
  AuthUrl:
    Description: Authorization URL for Cognito
    Value: !Sub "https://${Domain}.auth.${AWS::Region}.amazoncognito.com/oauth2/authorize"
    
  TokenUrl:
    Description: Token URL for Cognito
    Value: !Sub "https://${Domain}.auth.${AWS::Region}.amazoncognito.com/oauth2/token"
    
  JwksUrl:
    Description: JWKS URL for Cognito
    Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json"
    
  LogoutUrl:
    Description: Logout URL for Cognito
    Value: !Sub "https://${Domain}.auth.${AWS::Region}.amazoncognito.com/logout?client_id=${UserPoolClient}&logout_uri=https://${Domain}.com"
    
  ClientId:
    Description: Client ID for Cognito User Pool Client
    Value: !Ref UserPoolClient

For detailed information on the parameters and resources, refer to the CloudFormation documentation on Amazon Cognito. The CloudFormation stack outputs include the formatted parameters, excluding the client secret, necessary for configuring ShinyProxy.

In the Cognito console, navigate to User pools -> App integration -> App client list -> Hosted UI. Choose Cognito user pool as the identity provider, then save the changes. This will enable the hosted UI for the user pool.

ShinyProxy Configuration

Stop the ShinyProxy service:

# Ubuntu
$ sudo service shinyproxy stop
# Amazon Linux 2023
$ sudo systemctl stop shinyproxy

Update the ShinyProxy configuration file with the OIDC parameters:

proxy:
  title: ShinyProxy
  landing-page: /
  bind-address: 127.0.0.1
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 8080
  container-log-path: /var/log/shinyproxy # Path for container logs
  docker:
    port-range-start: 20000

  authentication: openid
  openid:
    roles-claim: cognito:groups
    auth-url: https://{COGNITO-DOMAIN}.auth.{AWS-REGION}.amazoncognito.com/oauth2/authorize
    token-url: https://{COGNITO-DOMAIN}.auth.{AWS-REGION}.amazoncognito.com/oauth2/token
    jwks-url: https://cognito-idp.{AWS-REGION}.amazonaws.com/{USER-POOL-ID}/.well-known/jwks.json
    logout-url: https://{COGNITO-DOMAIN}.auth.{AWS-REGION}.amazoncognito.com/logout?client_id={CLIENT-ID}&logout_uri={LOGOUT-URI}
    client-id: {CLIENT-ID}
    client-secret: ${CLIENT_SECRET} # Use an environment variable for the client secret

  specs:
    - id: shiny-app
      display-name: Ridge & Lasso App
      description: Demonstrates regularization effects on linear models
      container-image: {AWS-ACCOUNT-ID}.dkr.ecr.{AWS-REGION}.amazonaws.com/{ECR-REPO}:{IMAGE-TAG}
      access-groups: [admin_users, shiny_users] # Access restricted to admin and shiny_users groups
    - id: dash-app
      display-name: Sentiment Analysis App
      description: Analyzes text sentiment
      port: 8050
      container-image: {AWS-ACCOUNT-ID}.dkr.ecr.{AWS-REGION}.amazonaws.com/{ECR-REPO}:{IMAGE-TAG}
      target-path: "#{proxy.getRuntimeValue('SHINYPROXY_PUBLIC_PATH')}"
      access-groups: [admin_users, dash_users] # Access restricted to admin and dash_users groups

server:
  forward-headers-strategy: native
  secure-cookies: true

logging:
  file:
    name: shinyproxy.log

Similar to the simple authentication set up, the roles-claim parameter specifies the claim in the ID token that contains the user’s roles, determining access based on group membership.

Note: Ensure that the LOGOUT-URI matches exactly between the Cognito console and the ShinyProxy configuration to avoid redirect mismatches. Use the well-formatted output parameters from the CloudFormation stack to prevent errors—even a trailing slash difference can cause issues.

Placeholder Values

  • {COGNITO-DOMAIN}: Cognito User Pool domain (e.g., ourdomain)
  • {AWS-REGION}: AWS region where the Cognito User Pool is located (e.g., us-east-1)
  • {USER-POOL-ID}: ID of the Cognito User Pool
  • {LOGOUT-URI}: URI to redirect users after logout (e.g., https://ourdomain.com)
  • {CLIENT-ID}: Client ID of the Cognito User Pool application client
  • {AWS-ACCOUNT-ID}: AWS account ID
  • {ECR-REPO}: Name of the ECR repository
  • {IMAGE-TAG}: Tag of the Docker image stored in the ECR repository

Environment Variable

  • ${CLIENT_SECRET}: The Client Secret, set as an environment variable

Create a .env file to store the client secret:

$ sudo nano /etc/shinyproxy/.env
CLIENT_SECRET=...

Ensure the .env file is secure and not accessible to unauthorized users. Next, open the ShinyProxy service file:

# For both Ubuntu and Amazon Linux 2023
$ sudo nano /etc/systemd/system/shinyproxy.service 

Add the EnvironmentFile directive under the [Service] section to point to the .env file:

[Unit]
Description=ShinyProxy
...

[Service]
EnvironmentFile=/etc/shinyproxy/.env
...

Reload the systemd daemon:

# For both Ubuntu and Amazon Linux 2023
$ sudo systemctl daemon-reload

Restart ShinyProxy:

# Ubuntu
$ sudo service shinyproxy start
# Amazon Linux 2023
$ sudo systemctl start shinyproxy

Grouped User Authentication

Assign users to the appropriate groups, then test the authentication flow by accessing the ShinyProxy landing page. Users in the admin_users group should have access to both applications, while those in the dash_users and shiny_users groups should only have access to the Dash and Shiny applications, respectively.

With this setup, users can authenticate via Amazon Cognito and access the applications based on their group membership.

To further enhance the authentication experience and improve user communication, consider integrating Amazon SES. This service enables customized email notifications, such as password resets and account verification emails, thereby improving the overall user experience.

With these configurations, ShinyProxy is now well integrated with Amazon Cognito as the identity provider, offering secure, group-based access to both Shiny and Dash applications.

GitHub Repository

The scripts, templates, and configuration files are all available in the following GitHub repository: shinyproxy-aws-deploy.

Related