Skip to content

Commit f92cc66

Browse files
authored
MRG: Merge pull request #1 from octue/create-module
Create module
2 parents 9469775 + df6cdd5 commit f92cc66

File tree

9 files changed

+373
-2
lines changed

9 files changed

+373
-2
lines changed

.github/workflows/release.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# This workflow releases a new version of the Terraform module.
2+
3+
name: Release
4+
5+
# Only trigger when a pull request into main branch is merged.
6+
on:
7+
pull_request:
8+
types: [closed]
9+
branches:
10+
- main
11+
12+
jobs:
13+
release:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout Repository
17+
uses: actions/checkout@v4
18+
19+
- name: Get module version
20+
id: get-version
21+
run: echo "version=$(cat VERSION.txt)" >> $GITHUB_OUTPUT
22+
23+
- name: Create Release
24+
uses: actions/create-release@v1
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, no need to create your own.
27+
with:
28+
tag_name: ${{ steps.get-version.outputs.version }}
29+
release_name: ${{ github.event.pull_request.title }}
30+
body: ${{ github.event.pull_request.body }}
31+
draft: false
32+
prerelease: false
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# This workflow updates the pull request description with an auto-generated section containing the categorised commit
2+
# message headers of the pull request's commits. The auto generated section is enveloped between two comments:
3+
# "<!--- START AUTOGENERATED NOTES --->" and "<!--- END AUTOGENERATED NOTES --->". Anything outside these in the
4+
# description is left untouched. Auto-generated updates can be skipped for a commit if
5+
# "<!--- SKIP AUTOGENERATED NOTES --->" is added to the pull request description.
6+
7+
name: update-pull-request
8+
9+
on: [pull_request]
10+
11+
jobs:
12+
description:
13+
uses: octue/workflows/.github/workflows/generate-pull-request-description.yml@main
14+
secrets:
15+
token: ${{ secrets.GITHUB_TOKEN }}
16+
permissions:
17+
contents: read
18+
pull-requests: write

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ cython_debug/
165165
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166166
# and can be added to the global gitignore or merged into this file. For a more nuclear
167167
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
168-
#.idea/
168+
.idea/
169169

170170
# Ruff stuff:
171171
.ruff_cache/

README.md

+159-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,160 @@
1+
> [!NOTE]
2+
> This Terraform module can be deployed alongside the [terraform-octue-django-api](https://github.com/octue/terraform-octue-django-api)
3+
> module to create a cloud-based "branch/workspace deployment", or alone to create the buckets needed for a local
4+
> environment.
5+
16
# terraform-octue-django-api-buckets
2-
A Terraform module for deploying storage buckets for a Django API.
7+
A Terraform module for deploying Cloud Storage buckets for use with a Django API server running locally or in the cloud.
8+
9+
10+
# Infrastructure
11+
Deploying this module creates Cloud Storage buckets for a local or cloud environment. This infrastructure is [isolated
12+
from other environments' infrastructure](#environments). These buckets are deployed:
13+
- A public static assets bucket
14+
- A private assets bucket
15+
- An optional public assets bucket
16+
17+
18+
# Installation and usage
19+
Add the below blocks to your Terraform configuration and run:
20+
```shell
21+
terraform init
22+
terraform plan
23+
```
24+
25+
If you're happy with the plan, run:
26+
```shell
27+
terraform apply
28+
```
29+
and approve the run.
30+
31+
32+
## Environments
33+
The suggested way of managing environments is via [Terraform workspaces](https://developer.hashicorp.com/terraform/language/state/workspaces).
34+
You can get started right away with the `main` environment by removing the `environment` input to the module.
35+
36+
To create and used other environments, see the example configuration below. It contains a `locals` block that
37+
automatically generates the environment name from the name of the current Terraform workspace by taking the text after
38+
the final hyphen. This supports uniquely named environments in Terraform Cloud (which must be unique within the
39+
organisation) while keeping the environment prefix short but unique within your GCP project. For this to work well,
40+
ensure your Terraform workspace names are slugified.
41+
42+
For example, if your resource affix was `my-project` and your Terraform workspace was called `my-project-testing`, the
43+
environment would be called `testing` and your resources would be named like this:
44+
- Static assets bucket: `"my-project--static-assets--testing"`
45+
- Private assets bucket: `"my-project--private-assets--testing"`
46+
47+
48+
## Example configuration
49+
50+
```terraform
51+
# main.tf
52+
53+
terraform {
54+
required_version = ">= 1.8.0, <2"
55+
56+
required_providers {
57+
google = {
58+
source = "hashicorp/google"
59+
version = "6.28.0"
60+
}
61+
}
62+
}
63+
64+
65+
provider "google" {
66+
project = var.google_cloud_project_id
67+
region = var.google_cloud_region
68+
}
69+
70+
71+
# Get the environment name from the workspace.
72+
locals {
73+
workspace_split = split("-", terraform.workspace)
74+
environment = element(local.workspace_split, length(local.workspace_split) - 1)
75+
}
76+
77+
78+
module "octue_django_api" {
79+
source = "git::github.com/octue/terraform-octue-django-api.git?ref=0.1.0"
80+
project = var.google_cloud_project_id
81+
region = var.google_cloud_region
82+
resource_affix = var.resource_affix
83+
environment = local.environment
84+
}
85+
86+
87+
module "octue_django_api_buckets" {
88+
source = "git::github.com/octue/terraform-octue-django-api-buckets.git?ref=0.1.0"
89+
server_service_account_email = module.octue_django_api.server_service_account.email
90+
project = var.google_cloud_project_id
91+
resource_affix = var.resource_affix
92+
environment = local.environment
93+
}
94+
```
95+
96+
```terraform
97+
# variables.tf
98+
99+
variable "google_cloud_project_id" {
100+
type = string
101+
default = "<your-google-project-id>"
102+
}
103+
104+
105+
variable "resource_affix" {
106+
type = string
107+
default = "<name-of-your-api>"
108+
}
109+
```
110+
111+
## Dependencies
112+
- Terraform: `>= 1.8.0, <2`
113+
- Providers:
114+
- `hashicorp/google`: `~>6.28`
115+
- Google cloud APIs:
116+
- The Cloud Resource Manager API must be [enabled manually](https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com)
117+
before using the module
118+
- All other required google cloud APIs are enabled automatically by the module
119+
120+
121+
## Authentication
122+
The module needs to authenticate with google cloud before it can be used:
123+
124+
1. Create a service account for Terraform and assign it the `editor` and `owner` basic IAM permissions
125+
2. Download a JSON key file for the service account
126+
3. If using Terraform Cloud, follow [these instructions](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#using-terraform-cloud).
127+
before deleting the key file from your computer
128+
4. If not using Terraform Cloud, follow [these instructions](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#authentication-configuration)
129+
or use another [authentication method](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#authentication).
130+
131+
132+
## Destruction
133+
> [!WARNING]
134+
> If the `deletion_protection` input is set to `true`, it must first be set to `false` and `terraform apply` run before
135+
> running `terraform destroy` or any other operation that would result in the destruction or replacement of the Cloud
136+
> Storage buckets. Not doing this can lead to a state needing targeted Terraform commands and/or manual > configuration
137+
> changes to recover from.
138+
139+
Disable `deletion_protection` and run:
140+
```shell
141+
terraform destroy
142+
```
143+
144+
145+
# Input reference
146+
147+
| Name | Type | Required | Default |
148+
|--------------------------------|------------|----------|------------|
149+
| `server_service_account_email` | `string` | Yes | N/A |
150+
| `google_cloud_project_id` | `string` | Yes | N/A |
151+
| `resource_affix` | `string` | Yes | N/A |
152+
| `environment` | `string` | No | `"main"` |
153+
| `create_public_bucket` | `boolean` | No | `false` |
154+
| `deletion_protection` | `bool` | No | `true` |
155+
156+
See [`variables.tf`](/variables.tf) for descriptions.
157+
158+
159+
# Output reference
160+
There are no outputs.

VERSION.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0

google_apis.tf

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
locals {
2+
services = {
3+
iam = "iam.googleapis.com"
4+
}
5+
}
6+
7+
8+
resource "google_project_service" "services" {
9+
for_each = local.services
10+
project = var.google_cloud_project_id
11+
service = each.value
12+
disable_on_destroy = false
13+
14+
timeouts {
15+
create = "30m"
16+
update = "40m"
17+
}
18+
}

main.tf

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
terraform {
2+
required_version = ">= 1.8.0, <2"
3+
4+
required_providers {
5+
google = {
6+
source = "hashicorp/google"
7+
version = "~>6.28.0"
8+
}
9+
}
10+
}

storage.tf

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# STATIC BUCKET
2+
###############
3+
# Static bucket for long term cacheable application assets (eg favicons, logo images etc).
4+
5+
resource "google_storage_bucket" "static_assets" {
6+
name = "${var.resource_affix}--static-assets--${var.environment}"
7+
location = "EU"
8+
force_destroy = !var.deletion_protection
9+
uniform_bucket_level_access = true
10+
labels = {}
11+
cors {
12+
origin = ["*"]
13+
method = ["GET"]
14+
response_header = ["*"]
15+
max_age_seconds = 3600
16+
}
17+
}
18+
19+
20+
# Make static bucket contents public.
21+
resource "google_storage_bucket_iam_member" "static_assets_object_viewer" {
22+
bucket = google_storage_bucket.static_assets.name
23+
role = "roles/storage.objectViewer"
24+
member = "allUsers"
25+
}
26+
27+
28+
# Allow the server to administer what's on the staging bucket
29+
resource "google_storage_bucket_iam_member" "static_assets_object_admin" {
30+
bucket = google_storage_bucket.static_assets.name
31+
role = "roles/storage.objectAdmin"
32+
member = "serviceAccount:${var.server_service_account_email}"
33+
}
34+
35+
36+
# PRIVATE BUCKET
37+
################
38+
# Private bucket for user-editable assets. Note: CORS are set to allow direct uploads, enabling upload of files larger
39+
# than 32 mb (Cloud Run has a hard limit on file upload size).
40+
41+
resource "google_storage_bucket" "private_assets" {
42+
name = "${var.resource_affix}--private-assets--${var.environment}"
43+
labels = {}
44+
location = "EU"
45+
force_destroy = !var.deletion_protection
46+
uniform_bucket_level_access = false
47+
48+
cors {
49+
origin = ["*"]
50+
method = ["GET", "HEAD", "PUT"]
51+
response_header = ["*"]
52+
max_age_seconds = 3600
53+
}
54+
}
55+
56+
57+
resource "google_storage_bucket_iam_member" "private_assets_object_admin" {
58+
bucket = google_storage_bucket.private_assets.name
59+
role = "roles/storage.objectAdmin"
60+
member = "serviceAccount:${var.server_service_account_email}"
61+
}
62+
63+
64+
# PUBLIC BUCKET (OPTIONAL)
65+
##########################
66+
resource "google_storage_bucket" "public_assets" {
67+
count = var.create_public_bucket ? 1 : 0
68+
name = "${var.resource_affix}--public-assets--${var.environment}"
69+
location = "EU"
70+
force_destroy = !var.deletion_protection
71+
uniform_bucket_level_access = true
72+
labels = {}
73+
cors {
74+
origin = ["*"]
75+
method = ["GET"]
76+
response_header = ["*"]
77+
max_age_seconds = 3600
78+
}
79+
}
80+
81+
82+
# Make public bucket contents public.
83+
resource "google_storage_bucket_iam_member" "public_assets_object_viewer" {
84+
count = var.create_public_bucket ? 1 : 0
85+
bucket = google_storage_bucket.public_assets[0].name
86+
role = "roles/storage.objectViewer"
87+
member = "allUsers"
88+
}
89+
90+
91+
# Allow the server to administer what's on the public bucket.
92+
resource "google_storage_bucket_iam_member" "public_assets_object_admin" {
93+
count = var.create_public_bucket ? 1 : 0
94+
bucket = google_storage_bucket.public_assets[0].name
95+
role = "roles/storage.objectAdmin"
96+
member = "serviceAccount:${var.server_service_account_email}"
97+
}

variables.tf

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
variable "server_service_account_email" {
2+
type = string
3+
description = "The email address of the service account for running the Django server."
4+
}
5+
6+
7+
variable "google_cloud_project_id" {
8+
type = string
9+
description = "The ID of the GCP project to deploy resources in."
10+
}
11+
12+
13+
variable "resource_affix" {
14+
type = string
15+
description = "The affix to add to each resource controlled by this module."
16+
}
17+
18+
19+
variable "environment" {
20+
type = string
21+
default = "main"
22+
description = "The name of the environment to deploy the resources in (must be one word with no hyphens or underscores in). This can be derived from a Terraform workspace name and used to facilitate e.g. testing and staging environments alongside the production environment ('main')."
23+
}
24+
25+
26+
variable "create_public_bucket" {
27+
type = bool
28+
default = false
29+
description = "If `true`, create a public assets bucket."
30+
}
31+
32+
33+
variable "deletion_protection" {
34+
type = bool
35+
default = false
36+
description = "If `true`, disallow deletion of the cloud storage buckets. `terraform apply` must be run after setting this to `false` before `terraform destroy` will work."
37+
}

0 commit comments

Comments
 (0)