Skip to content

Commit

Permalink
Initial commit (#1)
Browse files Browse the repository at this point in the history
* Initial commit

* Incorporate feedback; squash bugs

* Refactor globals

* Fix listener SSL settings

* Resolve networking issues

* Unused imports, formatstring

* Documentation review

* Fix IAM role policy attachment bug

* Make routing options more flexible

* Update readme

* Add missing import to sample script

---------

Co-authored-by: Ryan Jung <[email protected]>
  • Loading branch information
ryanjjung and ryanjjung authored Sep 19, 2024
1 parent 3f6cdcd commit 00190e3
Show file tree
Hide file tree
Showing 13 changed files with 2,082 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.pyc
__pycache__
venv/
215 changes: 214 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,215 @@
# pulumi
Common Pulumi elements for use in Thunderbird infrastructure development

Common Pulumi elements for use in Thunderbird infrastructure development.

## Usage

Typically, you want to implement the classes defined in this module to define infrastructure
resources to support your application. These represent common infrastructural patterns which you can
customize to some degree.

See the [Documentation](#documentation) section below for details on how to include this in your
project and use each kind of resource.


## Pulumi setup

Our Pulumi code is developed against Python 3.12 or later. If this is not your default version, you'll need to manage your own virtual environment.

Check your default version:

```sh
$ python -V
Python 3.12.4
```

If you need a newer Python, [download and install it](https://www.python.org/downloads/). Then you'll have to set up the virtual environment yourself with something like this:

```sh
virtualenv -p /path/to/python3.12 venv
./venv/bin/pip install -r requirements.txt
```

After this, `pulumi` commands should work. If 3.12 is your default version of Python, Pulumi should set up its own virtualenv, and you should not have to do this.


## Start a new Pulumi project

### S3 bucket

Create an S3 bucket in which to store state for the project. Generally, you should follow this
naming scheme:

```
tb-$PROJECT_NAME-pulumi
```

One bucket can hold states for all of that project's stacks, so you only need to create the one
bucket per project.


### Repo setup

You probably already have a code repo with your application code in it. If not, create such a repo.

Create a directory there called `pulumi` and create a new project and stack in it. You'll need the
name of the S3 bucket from the previous step here. If you are operating in an AWS region other than
what is set as your default for AWSCLI, be sure to `export AWS_REGION=us-east-1` or whatever else
you may need to do to override that.

```sh
cd /path/to/pulumi/code
pulumi login s3://$S3_BUCKET_NAME
pulumi new aws-python
```

Follow the prompts to get everything named.


### Set up this module

Ensure your pulumi code directory contains a `requirements.txt` file with at least this repo listed:

```
git+https://github.com/thunderbird/pulumi.git
```

You can pin your code to a specific version of this module by appending `@branch_name` to that.

Pulumi will need these requirements installed. On your first run of a `pulumi preview` command (or
some others), Pulumi will attempt to set up its working environment. If this fails, or you need to
make adjustments later, you can activate Pulumi's virtual environment to perform pip changes.
Assuming Pulumi's virtual environment lives at `venv`, run:

```sh
./venv/bin/pip install -U -r requirements.txt
```

You can now develop Python Pulumi code in that directory, referring to this module with imports such
as these:

```python
import tb_pulumi

# ...or...

from tb_pulumi import (ec2, fargate, secrets)
```


### Use this module

When you issue `pulumi` commands (like "up" and "preview" and so on), it looks for a `__main__.py`
file in your current directory and executes the code in that file. To use this module, you'll import
it into that file and write up some code and configuration files. As a quickstart, you can copy
`__main__.py.example` and `config.stack.yaml.example` into your repo and begin to tweak them.


#### Create a config file

It is assumed that a config file will exist at `config.$STACK.yaml` where `$STACK` is the currently
selected Pulumi stack. This file must contain a mapping of names of config settings to their desired
values. Currently, only one such setting is recognized. That is `resources`.

This is a mostly arbitary mapping that you will have to interpret on your own (more on that later),
but some conventions are recommended. Namely:

- `resources` should be a mapping where the keys are the Pulumi type-strings for the resources
they are configuring. For example, if you want to build a VPC with several subnets, you
might use the `tb_pulumi.network.MultiCidrVpc` class. Following this convention, that should
be accompanied by a `tb:network:MultiCidrVpc` key in this mapping.
- The values these keys map to should themselves be mappings. This provides a convention where
more than one of each pattern are configurable. The keys here should be arbitrary but unique
identifiers for the resources being configured. F/ex: `backend` or `api`.
- The values these keys map to should be a mapping where the keys are valid configuration
options for the resources being built. The full listing of these values can be found by
browsing the [documentation](#documentation).


#### Define a ThunderbirdPulumiProject

In your `__main__.py` file, start with a simple skeleton (or use `__main__.py.example` to start):

```python
import tb_pulumi

project = tb_pulumi.ThunderbirdPulumiProject()
```

If you have followed the conventions outlined above, `project` is now an object with a key property,
`config`, which gives you access to the config file's data. You can use this in the next step to
feed parameters into resource declarations.


#### Declare ThunderbirdComponentResources

A `pulumi.ComponentResource` is a collection of related resources. In an effort to keep consistent
tagging and such across all Thunderbird infrastructure projects, the resources available in this
module all extend a custom class called a `ThunderbirdComponentResource`. If you have
followed the conventions outlined so far, it should be easy to stamp out common patterns with them
by passing config options into the constructors for these classes.


#### A brief example

You should be able to run through these steps to get a very simple working example:

- Set up a pulumi project and a stack called "foobar".
- `cp __main__.py.example /my/project/__main__.py`
- `cp config.stack.yaml.example /my/project/config.foobar.yaml`
- Tweak the config as you see fit

A `pulumi preview` should list out a few resources to be built. Depending on how you've configured
things, this could include:

- A VPC
- A subnet for each of the two CIDRs defined
- Internet or NAT Gateways
- Routes


## Documentation
<a name="documentation"></a>

Documentation for this module is currently maintained through this readme and the commentary in the
code. If you like, you can browse that commentary using pydoc:

```sh
virtualenv .venv
pip install -r requirements.txt
python -m pydoc -p 8080 .
```

Then click [this link](http://localhost:8080/tb_pulumi.html).


## Implementing ThunderbirdComponentResources

So you want to develop a new pattern to stamp out? Here's what you'll need to do:

- Determine the best place to put the code. Is there an existing module that fits the bill?
- Determine the Pulumi type string for it. This goes: `org:module:class`. The `org` will always
be "tb". The `module` will be the Python submodule you're placing the new class in. The
`class` is whatever you've called the class.
- Design the class following these guidelines:
- The constructor should always accept, before any other arguments, the following positional
options:
- **name:** The internal name of the resource as Pulumi tracks it.
- **project:** The ThunderbirdPulumiProject these resources belong to.
- The constructor should always accept the following keyword arguments:
- **opts:** A `pulumi.ResourceOptions` object which will get merged into the default set of
arguments managed by the project.
- The constructor should explicitly define only those arguments that you intend to have
default values which differ from the default values the provider will set, or which imply
larger patterns (such as "build_jumphost" implying other resources, like a security group
and its rules, not just an EC2 instance).
- The constructor may accept a final `**kwargs` argument with arbitrary meaning. Because the
nature of a component resource is to compile many other resources into one class, it is
not implicitly clear what "everything else" should apply to. If this is implemented, its
function should be clearly documented in the class.
- The class should extend `tb_pulumi.ThunderbirdComponentResource`.
- The class should call its superconstructor in the following way:
- `super().__init__(typestring, name, project, opts=opts)`
- Any resources you create should always be assigned a key in `self.resources`.
- Any resources you create must have the `parent=self` pulumi.ResourceOption set.
- At the end of the `__init__` function, you must call `self.finish()`
23 changes: 23 additions & 0 deletions __main__.py.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/env python3

import tb_pulumi
import tb_pulumi.network


# Create a project to aggregate resources. This will allow consistent tagging, resource protection,
# etc. The naming is derived from the currently selected Pulumi project/stack. A configuration file
# called `config.$stack.yaml` is loaded from the current directory. See config.stack.yaml.example.
project = tb_pulumi.ThunderbirdPulumiProject()

# Pull the "resources" config mapping
resources = project.config.get('resources')

# Let's say we want to build a VPC with some private IP space. We can do this with a `MultiCidrVpc`.
vpc_opts = resources['tb:network:MultiCidrVpc']['vpc']
vpc = tb_pulumi.network.MultiCidrVpc(
# project.name_prefix combines the Pulumi project and stack name to create a unique prefix
f'{project.name_prefix}-vpc',
# Add this module's resources to the project
project,
# Map the rest of the config file directly into this function call, separating code from config
**vpc_opts)
12 changes: 12 additions & 0 deletions config.stack.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
resources:
tb:network:MultiCidrVpc:
vpc:
cidr_block: 10.0.0.0/16
subnets:
us-east-1a:
- 10.0.101.0/24
us-east-1b:
- 10.0.102.0/24
us-east-1c:
- 10.0.103.0/24
54 changes: 54 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[project]
name = "tb_pulumi"
version = "0.0.1"
description = "Framework and patterns for using Pulumi at Thunderbird"
requires-python = ">3.12"
dynamic = ["dependencies"]

[project.urls]
repository = "https://github.com/thunderbird/pulumi.git"
issues = "https://github.com/thunderbird/pulumi/issues"

[tool.setuptools.dynamic]
dependencies = { file = ["requirements.txt"] }

# Ruff
[tool.ruff]
line-length = 120

# Exclude a variety of commonly ignored directories.
exclude = [
".eggs",
".git",
".ruff_cache",
".venv",
"__pycache__",
"__pypackages__",
"venv",
]

# Always generate Python 3.12-compatible code.
target-version = "py312"

[tool.ruff.format]
# Prefer single quotes over double quotes.
quote-style = "single"

[tool.ruff.lint]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F"]
ignore = []

# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"

[tool.ruff.lint.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
boto3>=1.34,<2.0
cryptography>=43.0.0,<44.0
pulumi>=3.130.0,<4.0.0
pulumi-aws>=6.0.2,<7.0.0
pulumi-random>=4.16,<5.0
# pyyaml is also a requirement, but is installed for us by pulumi
Loading

0 comments on commit 00190e3

Please sign in to comment.