2

I am trying to launch a jupyterlab instance using cloudformation (its something I do a lot and sagemaker does not have a 1y free tier) so the beginning looks like this which does not work. Specifically the password parameter

# AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a Jupyter Lab Instance with an Elastic Load Balancer


Parameters:
  KeyName:
    Description: >-
      Name of an existing EC2 KeyPair to enable SSH access to the instance
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: Must be the name of an existing EC2 KeyPair.
    Default: eduinstance
  VPC:
    Description: VPC ID of the VPC in which to deploy this stack.
    Type: AWS::EC2::VPC::Id
    ConstraintDescription: Must be the name of a valid VPC
    Default: vpc-10a7ac6a
  Subnets:
    Type: List<AWS::EC2::Subnet::Id>
    Default: subnet-8cde25d3,subnet-531fda72,subnet-4bbe3006
    Description: >-
      Subnets for the Elastic Load Balancer.
      Please include at least two subnets
  Password: 
    Type: String
    NoEcho: false
    MinLength: 4
    Default: '{{resolve:ssm:JLabPassword:1}}'
    Description: Password to set for Jupyter Lab
  EBSVolumeSize:
    Type: Number
    Description: EBS Volume Size (in GiB) for the instance
    Default: 8
    MinValue: 8
    MaxValue: 64000
    ConstraintDescription: Please enter a value between 8 GB and 64 TB
  EC2InstanceType:
    Type: String
    Default: t2.micro
    AllowedValues:
      - t2.micro
      - c5.large
      - m5.large
    Description: Enter t2.micro, c5.large or m5.large. Default is t2.micro.


Conditions:
  JupyterPasswordDefault: !Equals
    - !Ref Password
    - DEFAULT

Resources:
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: 
      IpAddressType: ipv4
      Scheme: internet-facing
      SecurityGroups: 
        - !GetAtt [ALBSG, GroupId]
      Subnets: !Ref Subnets
      Type: application
  
  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref ALBTargetGroup
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
  
  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 8888
      Protocol: HTTP
      Targets: 
        - Id: !Ref ComputeInstance
      TargetType: instance
      VpcId: !Ref VPC
  
  ComputeInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref ComputeIAMRole

  
  ComputeInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t2.micro
      SubnetId: !Select [0, !Ref Subnets]
      KeyName: !Ref KeyName
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:33}}'
      SecurityGroupIds: 
        - !GetAtt [ComputeSG, GroupId]
      IamInstanceProfile: !Ref ComputeInstanceProfile
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: !Ref EBSVolumeSize
            DeleteOnTermination: true
      UserData:
        Fn::Base64: !Sub
          - |
            #!/bin/bash
            yum update -y
            yum install python3-pip -y
            yum install java-1.8.0-openjdk -y
            cd /home/ec2-user/
            wget https://repo.anaconda.com/archive/Anaconda3-2020.11-Linux-x86_64.sh
            sudo -u ec2-user bash Anaconda3-2020.11-Linux-x86_64.sh -b -p /home/ec2-user/anaconda
            echo "PATH=/home/ec2-user/anaconda/bin:$PATH" >> /etc/environment
            source /etc/environment
            jupyter notebook --generate-config
            mkdir .jupyter
            cp /root/.jupyter/jupyter_notebook_config.py /home/ec2-user/.jupyter/
            echo "c = get_config()" >> .jupyter/jupyter_notebook_config.py
            echo "c.NotebookApp.ip = '*'" >> .jupyter/jupyter_notebook_config.py
            NB_PASSWORD=$(python3 -c "from notebook.auth import passwd; print(passwd('${password}'))")
            echo "c.NotebookApp.password = u'$NB_PASSWORD'" >> .jupyter/jupyter_notebook_config.py
            rm Anaconda3-2020.11-Linux-x86_64.sh
            mkdir Notebooks
            chmod 777 -R Notebooks .jupyter
            su -c "jupyter lab" -s /bin/sh ec2-user
          - password: !Ref Password #!If [JupyterPasswordDefault, '{{resolve:ssm:JupyterLabPassword:1}}', !Ref Password]

  
  ALBSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Security Group for JupyterLab ALB. Created Automatically.
      SecurityGroupIngress: 
        - CidrIp: 0.0.0.0/0
          Description: Allows HTTP Traffic from anywhere
          FromPort: 80
          ToPort: 80
          IpProtocol: tcp

  ComputeSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Security Group for JupyterLab EC2 Instance. Created Automatically.
      SecurityGroupIngress: 
        - Description: Allows JupyterLab Server Traffic from ALB.
          FromPort: 8888
          IpProtocol: tcp
          SourceSecurityGroupId: !GetAtt [ALBSG, GroupId]
          ToPort: 8890
        - CidrIp: 0.0.0.0/0
          Description: Allows SSH Access from Anywhere
          FromPort: 22
          ToPort: 22
          IpProtocol: tcp
  
  ComputeIAMRole:
    Type: AWS::IAM::Role
    Properties: 
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Description: Allows EC2 Access to S3. Created Automatically.
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/AmazonSageMakerFullAccess

Outputs:
  URL:
    Description: URL of the ALB
    Value: !Join 
      - ''
      - - 'http://'
        - !GetAtt 
          - ALB
          - DNSName
  
  ConnectionString:
    Description: Connection String For SSH On EC2
    Value: !Join
      - ''
      - - 'ssh -i "'
        - !Ref KeyName
        - '.pem" ec2-user@'
        - !GetAtt
          - ComputeInstance
          - PublicDnsName

It however interprets the string literally so I don't actually get my password but the resolve... itself.

Screenshot of the Actual Parameter

2 Answers2

2

Based on the comments and new, updated template by OP, and to expand on @DennisTraub answer.

SSM parameters resolve in almost all cases in the template, with the exception of UserData (btw, Init will also not work). This means, that dynamic reference will not resolve when used in the context of UserData. This is due to security issues.

UserData can be read in plain text by anyone who can view basic attributes of the instance. This means, that your JLabPassword would be in plain text available in UserData for everyone to see, if such resolution would be possible.

To rectify the issue, the SSM parameters should be used in UserData as follows:

  1. Attach IAM permission ssm:GetParameter to the instance role/profile which allows instance to access the SSM Parameter Store.

  2. Instead on {{resolve:ssm:JLabPassword:1}} in your Parameter, you can just pass JLabPassword so that the name of the SSM paramtter gets passed into the UserData, not the actual value of it.

  3. In the UserData, please use AWS CLI get-parameter to get the actual value of your JLabPassword.

The above ensures that the value of JLabPassword is kept private and not visible in plain text in UserData.

Marcin
  • 215,873
  • 14
  • 235
  • 294
  • 2
    Thank you very much! This seems to be the issue. The way I solved it was firstly set the parameter name as the default password (JLabPassword). Then I used a conditional to check if the password is equal to the default password. If it was I used the following command to get the password: $(aws ssm get-parameter --name ${PasswordParameter} --region ${AWS::Region} --output text | awk '{print $(NF-1)}') and substituted the parameter and region through the template. After that I added the IAM Policy to Allow ssm:GetParameter on resource '*" and attached it to the instance's IAM Role. – Kaizad Wadia Dec 26 '20 at 05:35
  • 1
    @KaizadWadia No problem. Glad it worked out:-) – Marcin Dec 26 '20 at 05:36
  • Glad you found a solution. Great team work! – Dennis Traub Dec 26 '20 at 16:09
1

Your passwort parameter's default value is missing the service name (ssm) as well as single quotes.

// What you have:
Password:
  Default: {{resolve:JupyterPassword:1}}
  ...

// What it should be:
Password:
  Default: '{{resolve:ssm:JupyterPassword:1}}'
  ...

Update: You've fixed the code in your question. Did my answer and the comments below solve your question? If not, I'm not sure what else you need.

Dennis Traub
  • 50,557
  • 7
  • 93
  • 108
  • Hi, thanks a lot for answering. TBH that wasn't really my code I changed it to make it work but when its actually Default: {{resolve:ssm:JupyterLabPassword:1}} what I get in cloudformation with a red alert: Template format error: [/Parameters/Password/Default] map keys must be strings; received a map instead – Kaizad Wadia Dec 25 '20 at 22:32
  • @KaizadWadia It should be `Default: '{{resolve:ssm:JupyterPassword:1}}'` - single quotes should be there. – Marcin Dec 25 '20 at 23:27
  • The problem was that it was not written in quotes but just curly braces... Another parameter I have (that works) also does this... The problem is that when put in quotes the whole thing is just taken literally and I still don't get my parameter :(. @Marcin – Kaizad Wadia Dec 25 '20 at 23:28
  • @KaizadWadia I assure you that it works as intendet. Please update your question with **exactly** code you are using. – Marcin Dec 25 '20 at 23:29
  • @Marcin I updated the question to include my new problem although you were right in saying it should be in quotes. – Kaizad Wadia Dec 25 '20 at 23:41
  • @KaizadWadia Your `Password` is correct now. It will be resolved inside the template, but parameter value will still show it as a literal string for security reasons. – Marcin Dec 25 '20 at 23:44
  • @Marcin No its not... I just deployed it and when I put in pass1234 it doesn't work but {{resolve:ssm:JLabPassword:0}} and it does... – Kaizad Wadia Dec 26 '20 at 00:22
  • If you don't believe me I just pasted my entire code you can try to deploy it yourself. – Kaizad Wadia Dec 26 '20 at 00:23
  • P.S. What security reasons could there be? As far as SSM is concerned its just some random string its not encrypted or anything just plaintext. – Kaizad Wadia Dec 26 '20 at 00:29
  • @KaizadWadia Your template shows that you are using incorrect paramter: `'{{resolve:ssm:JLabPassword:0}}'`. There is no version 0 on your screenshot. – Marcin Dec 26 '20 at 02:24
  • @Marcin huh, that should have been a 1. It still does not resolve though. Very strange considering the EC2 instance AMI loads fine. – Kaizad Wadia Dec 26 '20 at 03:26
  • @KaizadWadia Let me make an answer, now I see the full template. Could provide it earlier if you've included the template from the start. – Marcin Dec 26 '20 at 03:34
  • Okay, thanks again for your help. I apologize for not including the template from the start. I thought this would be a minor syntax issue with a quick fix. @Marcin – Kaizad Wadia Dec 26 '20 at 03:40