S3PyPI

repository·master·Indexed 43 days ago

https://github.com/gorilla-co/s3pypi

A CLI tool and library for hosting a private, low-cost Python Package Index (PyPI) using an AWS S3 bucket and Amazon CloudFront. Version 2.0.1 provides commands to upload and delete packages, manage root indices, and handle concurrency locking via DynamoDB. It includes a Python API for programmatic package distribution management and Terraform configurations for provisioning the required AWS infrastructure.

Tokens
6.1K
Snippets
25
Records
29
Agent score
56%

What's inside S3PyPI

  1. Migrate from s3pypi 0.x to 1.x

    master

    If you are upgrading from version 0.x (which used CloudFormation), you must import your existing resources into Terraform and rename index files to support the new CloudFront-to-S3-REST-API architecture.

    1. Import resources into Terraform:
    $ terraform init
    $ terraform import module.s3pypi.aws_s3_bucket.pypi example-bucket
    $ terraform import module.s3pypi.aws_cloudfront_distribution.cdn EDFDVBD6EXAMPLE
    $ terraform apply
    1. Rename index objects: Because the new configuration uses the S3 REST API instead of the S3 website endpoint, all <package>/index.html objects must be renamed to <package>/. Use the provided migration script:
    $ scripts/migrate-s3-index.py example-bucket

    Note: If you prefer to keep using the old publicly accessible S3 website endpoint, use the --index.html flag and set --s3-put-args='ACL=public-read' during upload.

  2. Manage users with Basic Authentication

    master

    If you enabled enable_basic_auth in your Terraform configuration, you can manage users using the basic_auth/put_user.py script. This script creates a parameter in AWS Systems Manager Parameter Store for the specified domain and user, storing a hashed password and salt.

    $ basic_auth/put_user.py pypi.example.com alice
    Password:
  3. Install packages from your S3PyPI repository

    master

    You can install packages from your private repository using pip by specifying the --extra-index-url. If you used a --prefix during upload, include it in the URL.

    Via CLI:

    $ pip install your-project --extra-index-url https://pypi.example.com/PREFIX/

    Via pip configuration (~/.pip/pip.conf):

    [global]
    extra-index-url = https://pypi.example.com/PREFIX/
  4. Upload Python packages with s3pypi upload

    master

    To distribute your packages, first build your distribution files (e.g., using sdist and bdist_wheel), then use the s3pypi upload command to push them to your S3 bucket.

    $ cd /path/to/your-project/
    $ python setup.py sdist bdist_wheel
    
    $ s3pypi upload dist/* --bucket example-bucket [--prefix PREFIX]
  5. Set up S3 and CloudFront using Terraform

    master

    To host your repository, you need an S3 bucket for storage and a CloudFront distribution for HTTPS. Use the provided Terraform configuration in the terraform/ directory to provision these resources.

    1. Clone the repository.
    2. Navigate to the terraform/ directory.
    3. Run terraform init and terraform apply.

    During terraform apply, you will be prompted for your AWS region, S3 bucket name, and CloudFront domain name. Alternatively, you can provide these in a config.auto.tfvars file.

    $ git clone https://github.com/gorilla-co/s3pypi.git
    $ cd s3pypi/terraform/
    
    $ terraform init
    $ terraform apply
  6. Configure Terraform for S3PyPI

    master

    You can customize your deployment by creating a config.auto.tfvars file. Key configuration options include:

    • region: Your desired AWS region.
    • bucket: The name of your S3 bucket.
    • domain: The domain name for your CloudFront distribution.
    • use_wildcard_certificate: Set to true if using a wildcard certificate from AWS Certificate Manager.
    • enable_dynamodb_locking: Set to true to enable distributed locking via DynamoDB to prevent concurrent upload conflicts.
    • enable_basic_auth: Set to true to enable basic authentication via Lambda@Edge and AWS Systems Manager Parameter Store.
    • waf_web_acl_arn: The ARN of an existing AWS WAFv2 Web ACL (must be in us-east-1 with scope CLOUDFRONT).
    region = "eu-west-1"
    bucket = "example-bucket"
    domain = "pypi.example.com"
    
    # Optional settings
    use_wildcard_certificate = true
    enable_dynamodb_locking  = true
    enable_basic_auth        = true
  7. Use s3pypi as a Terraform module

    master

    You can integrate S3PyPI into your own infrastructure code by using it as a Terraform module. This allows you to manage the S3 bucket, CloudFront distribution, and associated security settings (like WAF) alongside your existing resources.

    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 4.0"
        }
      }
    }
    
    provider "aws" {
      region = "eu-west-1"
    }
    
    provider "aws" {
      alias  = "us_east_1"
      region = "us-east-1"
    }
    
    module "s3pypi" {
      source = "github.com/gorilla-co/s3pypi//terraform/modules/s3pypi"
    
      bucket = "example-bucket"
      domain = "pypi.example.com"
    
      use_wildcard_certificate = true
      enable_dynamodb_locking  = true
      enable_basic_auth        = true
    
      providers = {
        aws.us_east_1 = aws.us_east_1
      }
    }
  8. Configure LockerConfig for retry behavior

    master

    The LockerConfig dataclass controls how the DynamoDBLocker behaves when it encounters a lock already held by another process.

    FieldTypeDefaultDescription
    retry_delayint1The number of seconds to sleep between attempts.
    max_attemptsint10The total number of attempts to acquire the lock before failing.
    from s3pypi.locking import LockerConfig
    
    # Wait 5 seconds between attempts, try up to 20 times
    cfg = LockerConfig(retry_delay=5, max_attempts=20)
  9. Configure S3 storage with S3Config

    master

    Use the S3Config dataclass to define the connection parameters for your S3 backend. This includes bucket details, prefixes, AWS profiles, regions, and custom endpoint URLs (useful for S3-compatible storage like MinIO).

    Key configuration options:

    • bucket: The name of the S3 bucket.
    • prefix: An optional prefix to scope all operations within a sub-directory.
    • profile: AWS CLI profile name.
    • region: AWS region.
    • no_sign_request: Set to True to use unsigned requests (useful for public buckets).
    • endpoint_url: Custom endpoint for S3-compatible services.
    • put_kwargs: A dictionary of extra arguments passed to S3 .put() calls.
    • index_html: Set to True to enable HTML index generation.
    • locks_table: The name of the DynamoDB table used for locking. If not provided, it defaults to {bucket}-locks.
    from s3pypi.storage import S3Config
    
    cfg = S3Config(
        bucket="my-pypi-bucket",
        prefix="packages/",
        region="us-east-1",
        index_html=True,
        locks_table="my-custom-locks-table"
    )
  10. Configure IAM permissions for s3pypi

    master

    The s3pypi CLI requires specific IAM permissions to interact with S3 and DynamoDB (if locking is enabled). Ensure the identity running the CLI has the following policy (replace example-bucket with your actual bucket name):

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:GetObject",
            "s3:PutObject",
            "s3:DeleteObject"
          ],
          "Resource": "arn:aws:s3:::example-bucket/*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "s3:ListBucket"
          ],
          "Resource": "arn:aws:s3:::example-bucket"
        },
        {
          "Effect": "Allow",
          "Action": [
            "dynamodb:GetItem",
            "dynamodb:PutItem",
            "dynamodb:DeleteItem"
          ],
          "Resource": "arn:aws:dynamodb:*:*:table/example-bucket-locks"
        }
      ]
    }
  11. Initialize a DynamoDBLocker using build()

    master

    To use DynamoDB for concurrency locking, use the DynamoDBLocker.build() static method. This method handles the creation of the DynamoDB resource and table connection.

    Parameters:

    • session: A boto3.session.Session instance.
    • table_name: The name of the DynamoDB table used for locking.
    • discover (bool, default False): If True, the locker will attempt to check if the table exists. If the table is not found, it returns a DummyLocker (which performs no actual locking) instead of raising an error.
    • cfg (LockerConfig, default LockerConfig()): Configuration for retry logic.

    LockerConfig options:

    • retry_delay (int): Seconds to wait between lock acquisition attempts (default 1).
    • max_attempts (int): Maximum number of attempts to acquire the lock before raising a timeout error (default 10).
    import boto3
    from s3pypi.locking import DynamoDBLocker, LockerConfig
    
    session = boto3.Session()
    config = LockerConfig(retry_delay=2, max_attempts=5)
    
    locker = DynamoDBLocker.build(
        session=session,
        table_name="my-lock-table",
        discover=True,
        cfg=config
    )
    
    with locker("package-name"):
        # Critical section
        pass