Heroku: Python dependencies in private repos without storing my password

后端 未结 4 1826
挽巷
挽巷 2021-02-19 02:36

The Problem

My problem is exactly like How do I install in-house requirements for Python Heroku projects? and How to customize pip's requirements.txt in Heroku on

相关标签:
4条回答
  • 2021-02-19 02:48

    I created a buildpack to solve this problem using a custom ssh key stored as an environment variable. As the buildpack is technology agnostic, it can be used to download dependencies using any tool like composer for php, bundler for ruby, npm for javascript, etc: https://github.com/simon0191/custom-ssh-key-buildpack

    1. Add the buildpack to your app:

      $ heroku buildpacks:add --index 1 https://github.com/simon0191/custom-ssh-key-buildpack
      
    2. Generate a new SSH key (lets say you named it deploy_key)

    3. Add the public key to your private repository account. For example:

      • Github: https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/

      • Bitbucket: https://confluence.atlassian.com/bitbucket/add-an-ssh-key-to-an-account-302811853.html

    4. Encode the private key as a base64 string and add it as the CUSTOM_SSH_KEY environment variable of the heroku app.

    5. Make a comma separated list of the hosts for which the ssh key should be used and add it as the CUSTOM_SSH_KEY_HOSTS environment variable of the heroku app.

      # MacOS
      $ heroku config:set CUSTOM_SSH_KEY=$(base64 --input ~/.ssh/deploy_key) CUSTOM_SSH_KEY_HOSTS=bitbucket.org,github.com
      # Ubuntu
      $ heroku config:set CUSTOM_SSH_KEY=$(base64 ~/.ssh/deploy_key) CUSTOM_SSH_KEY_HOSTS=bitbucket.org,github.com
      
    6. Deploy your app and enjoy :)
    0 讨论(0)
  • 2021-02-19 02:48

    I faced the same problem. Like you, I am amazed how difficult it is to find good documentation on how to install private dependency (whatever the language and the service used).

    Because this is not a main concern of service providers, I now try a systematic approach relying as few as possible on idiosyncratic features. I try to find the easier solution for each of these steps:

    • Pass the credentials to the build environment using a secure channel. For python, use an environment variable containing a SSH key as a base64 string. For js, same with the npm token.
    • Configure the build process to use these credentials. In the best case it involves configuring ssh to use a deploy key. Otherwise it can as basic as cloning the dependency for later use. For your specific case with python and heroku, you can use the hook 'pre_compile'.

    I detailed the process for my future self here: https://gist.github.com/michelbl/a6163522d95540cf0c8b6667bd35d5f5


    I need to give access to a private dependency. It can happen for continuous integration or deployment.

    Here we use python and github, using the services CircleCI and Heroku. However, the principles applies everywhere.

    What is a deploy key?

    See https://developer.github.com/v3/guides/managing-deploy-keys/

    There are 4 ways of granting access to a private dependency, but deploy keys are a good compromise in term of security and ease of use for projects that do not require too many dependencies (in that case, prefer a machine user). In any case, do not use username/password of a developer account or oauth token as they do not provide privilege limitation.

    Create a deploy key:

    ssh-keygen -t rsa -b 4096 -C "myself@my_company.com"
    

    Give the public part to gihub.

    Give the private part to the service needing access. See below.

    General strategy

    Whatever the service or the technology that I use, the goal is to access the git repo using ssh, using the deploy key.

    Obviously, I do not want to put the deploy key in the repo. But most services (CI, deployment) provide a way to set protected environment variables that can be used at build time. The key can be encoded using base64:

    cat deploy-key | base64
    cat deploy-key.pub | base64
    

    Most services also provide a way to tailor the build procedure. This is needed to configure ssh to use the deploy key.

    CircleCI

    Set the deploy key using env variables, encode with base64.

    In config.yml, add a step:

    echo $DEPLOY_KEY_PRIVATE | base64 --decode > ~/.ssh/deploy-key
    chmod 400 ~/.ssh/deploy-key
    echo $DEPLOY_KEY_PUBLIC | base64 --decode > ~/.ssh/deploy-key.pub
    ssh-add ~/.ssh/deploy-key
    
    # Run this to check which private key is used. If the checkout key is used,
    # github replies "Hi my_org/my_package". If the deploy key is used as wished,
    # github replies "Hi my_org/my_dependency".
    #ssh -i ~/.ssh/deploy-key -T git@github.com || true
    
    # Now pip connects to git+ssh using the deploy key
    export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy-key"
    
    pip install -r requirements.txt
    

    requirements.txt can be something like:

    # The purpose of this file is to install the private dependency *before*
    # setup.py is run.
    
    # Be sure ssh is configured to use a ssh key with read permission to the repo.
    git+ssh://git@github.com/my_org/my_dependency@1.0.10
    
    # Run setup.py. The private dependency is already installed with the good
    # version so pip doesn't try to fetch it from PyPI.
    --editable .
    

    and setup.py does not care about the dependency beeing private:

    from distutils.core import setup
    
    setup(
        name='my_package',
        version='1.0',
        packages=[
            'my_package',
        ],
        install_requires=[
            # Beware, the following package is a private dependency.
            # Python provides several way to install private dependencies, none
            # are really satisfactory.
            # 1. Use dependency_links / --process-dependency-links. Good luck with
            #    that!
            # 2. Maintain a private package repository. Good luck with that!
            # 3. Install the private dependency separately before setup.py is run.
            #    This is now the prefered way. Be sure that ssh is properly
            #    configured to use a ssh key with read permission to the github repo
            #    of the private dependency, then run:
            #    `pip install -r requirements.txt`
            'my_dependency==1.0.10',
            ... # my normal dependencies
            'unidecode==1.0.22',
            'uwsgi==2.0.15',
            'nose==1.3.7', # tests
            'flake8==3.5.0', # style
        ],
    )
    

    Heroku

    For python, there is no need to write a custom buildpack. First, set the deploy key using env variables, encode with base64.

    Then add the hook bin/pre_compile:

    # This script configures ssh on Heroku to use the deploy key.
    # This is needed to install private dependencies.
    #
    # Note that this does not work with Heroku review apps. Indeed review apps can
    # inherits env variables from their parents, but they access their values after
    # the build. You would need a way to pass the ssh key to this script another
    # way.
    #
    # See also
    # * https://stackoverflow.com/questions/21297755/heroku-python-dependencies-in-private-repos-without-storing-my-password#
    # * https://github.com/bjeanes/ssh-private-key-buildpack
    
    # Ensure we have an ssh folder
    if [ ! -d ~/.ssh ]; then
      mkdir -p ~/.ssh
      chmod 700 ~/.ssh
    fi
    
    # Create the key files
    cat $ENV_DIR/DEPLOY_KEY | base64 --decode > ~/.ssh/deploy-key
    chmod 400 ~/.ssh/deploy-key
    cat $ENV_DIR/DEPLOY_KEY | base64 --decode > ~/.ssh/deploy-key.pub
    #ssh-add ~/.ssh/deploy-key
    
    # If you want to disable host verification, you could use that.
    #ssh -oStrictHostKeyChecking=no -T git@github.com 2>&1
    
    # Run that if you want to check that ssh uses the correct key.
    #ssh -i ~/.ssh/deploy-key -T git@github.com || true
    
    # Configure ssh to use the correct deploy key when connecting to github.
    # Disables host verification.
    echo -e "Host github.com\n"\
            "  IdentityFile ~/.ssh/deploy-key\n"\
            "  IdentitiesOnly yes\n"\
            "  UserKnownHostsFile=/dev/null\n"\
            "  StrictHostKeyChecking no"\
            >> ~/.ssh/config
    
    # Unfortunately this does not seem to work.
    #export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy-key"
    
    # The vanilla python buildpack can now install all the dependencies in
    # requirement.txt
    
    0 讨论(0)
  • 2021-02-19 03:01

    Create a private PyPI server

    If you create your own PyPI server, you can simply list your packages in your requirements.txt file and then store the url for your server (including username and password) in the config variable, PIP_EXTRA_INDEX_URL.

    For example:
    heroku config:set PIP_EXTRA_INDEX_URL='https://username:password@privateserveraddress.com/simple'

    Note that this is the same as using the pip install command line option, --extra-index-url. (See https://pip.pypa.io/en/stable/user_guide/#environment-variables) The primary index url will still be the default (https://pypi.org/simple). This means that pip will first attempt to resolve package names in your requirements file at the default PyPI server, and then try your private server second.

    If you need packages in your private server that have the same name as packages in PyPI, then you need the primary index url to be your server and the --extra-index-url option to be the default server's url. You would need to do this if you want to host your own version of an existing package without changing the package name. I haven't tried this, but it currently looks like you would need to to create a fork of heroku's official python buildpack and make a small change to the bin/steps/pip-install file.

    The reason pip has access to the PIP_EXTRA_INDEX_URL is because of this block in that file:

        # Set Pip env vars
        # This reads certain environment variables set on the Heroku app config
        # and makes them accessible to the pip install process.
        #
        # PIP_EXTRA_INDEX_URL allows for an alternate pypi URL to be used.
        if [[ -r "$ENV_DIR/PIP_EXTRA_INDEX_URL" ]]; then
            PIP_EXTRA_INDEX_URL="$(cat "$ENV_DIR/PIP_EXTRA_INDEX_URL")"
            export PIP_EXTRA_INDEX_URL
            mcount "buildvar.PIP_EXTRA_INDEX_URL"
        fi
    

    Code like this is necessary to read config variables in buildpacks (see https://devcenter.heroku.com/articles/buildpack-api#buildpack-api), but you should be able to simply duplicate this codeblock, replacing PIP_EXTRA_INDEX_URL with PIP_INDEX_URL. Then set PIP_INDEX_URL to your private server's url and PIP_EXTRA_INDEX_URL to the default PyPI url.

    If you are using another source instead of a private PyPI server, such as github, and simply need a way to avoid hardcoding a username and password in your requirements.txt file, then also note that you can use environment variables in requirements.txt (see https://pip.pypa.io/en/stable/reference/pip_install/#using-environment-variables). You would just have to export them in bin/steps/pip-install as you would for PIP_INDEX_URL.

    0 讨论(0)
  • 2021-02-19 03:10

    You could use a pre-compile step as described here to run something like M4 to do substitutions on your requirements.txt to file in the password from the environment variable.

    0 讨论(0)
提交回复
热议问题