Skip to content

Commit

Permalink
Add pulumi to the project to start deploying infrastructure
Browse files Browse the repository at this point in the history
We're using Pulumi here to deploy two things

- A Cloudfront distribution
- An OIDC provider

This required a small amount of reconfiguration of the linters to allow for the
slightly different settings that Pulumi requires.

We've added pulumi to the dev container so that it can be run from there.

We're also adding a GitHub Actions workflow to deploy the infrastructure
when the code is pushed to the main branch, using the OIDC provider (which has
been applied manually to bootstrap the process).
  • Loading branch information
iainlane committed Jun 7, 2024
1 parent b8dea17 commit 0880121
Show file tree
Hide file tree
Showing 18 changed files with 1,592 additions and 26 deletions.
4 changes: 4 additions & 0 deletions .devcontainer/Dockerfile.devcontainer
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ FROM ghcr.io/iainlane/dotfiles-rust-tools:git-24a7c0cfa3e9b909f954a85dd0b4163f60

FROM public.ecr.aws/aws-cli/aws-cli:2.16.3 AS aws-cli

FROM pulumi/pulumi-base:3.118.0 AS pulumi

FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:1-22-bookworm

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
Expand All @@ -13,6 +15,8 @@ RUN ln -s /usr/local/aws-cli/v2/current/bin/aws /usr/local/bin/aws

COPY --from=rust-tools /usr/local/bin/* /usr/local/bin/

COPY --from=pulumi /pulumi/bin/* /usr/bin/

RUN corepack enable

USER node
5 changes: 4 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"source=${localWorkspaceFolderBasename}-node_modules-met.no,target=${containerWorkspaceFolder}/gen/met.no/node_modules,type=volume",
"source=${localWorkspaceFolderBasename}-node_modules-geojs,target=${containerWorkspaceFolder}/gen/geojs/node_modules,type=volume",
"source=${localWorkspaceFolderBasename}-yarn_cache,target=/home/node/.cache/yarn,type=volume",
"source=${localWorkspaceFolderBasename}-node_cache,target=/home/node/.cache/node,type=volume"
"source=${localWorkspaceFolderBasename}-node_cache,target=/home/node/.cache/node,type=volume",
"source=${localWorkspaceFolderBasename}-pulumi-config,target=/home/node/.pulumi,type=volume"
],

"containerUser": "node",
Expand All @@ -20,6 +21,8 @@
"AWS_SDK_LOAD_CONFIG": "true"
},

"runArgs": ["--env-file", ".devcontainer/.env"],

"onCreateCommand": "${containerWorkspaceFolder}/.devcontainer/onCreateCommand.sh",

"customizations": {
Expand Down
3 changes: 2 additions & 1 deletion .devcontainer/onCreateCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ sudo chown node:node /home/node/.cache
sudo chown node:node /home/node/.cache/node
sudo chown node:node /home/node/.cache/yarn
sudo chown node:node node_modules */*/node_modules
sudo chown node:node /home/node/.pulumi

yarn install --immutable < /dev/null
yarn serverless dynamodb install
yarn serverless dynamodb install --stage=local
59 changes: 59 additions & 0 deletions .github/workflows/oidc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
on:
pull_request:

push:
branches:
- main

name: Authenticate with AWS

jobs:
oidc:
permissions:
contents: read
id-token: write
pull-requests: write

runs-on: ubuntu-latest

env:
AWS_REGION: eu-west-2
STATE_BUCKET: coldoutsi.de-pulumi-state

steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
audience: coldoutsi.de-dev
aws-region: ${{ env.AWS_REGION }}
role-to-assume: arn:aws:iam::072248381277:role/oidcRole-715afe8

- name: Print session info
run: aws sts get-caller-identity

- name: Check out
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6

- name: Enable corepack
run: |
corepack enable
- name: Set up Node.js
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: "package.json"
cache: "yarn"

- name: Install dependencies
run: yarn workspace pulumi install --immutable

- name: Pulumi preview
uses: pulumi/actions@18b5a33fc447ab919feb61f2bb41147a1b30ab40 # v5.2.4
with:
cloud-url:
s3://${{ env.STATE_BUCKET }}?region=${{ env.AWS_REGION }}&awssdk=v2
stack-name: organization/coldoutsi.de/dev
command: preview
comment-on-pr: true
comment-on-summary: true
work-dir: pulumi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ coverage/

# Personal settings
.vscode/settings.json
.devcontainer/.env

# Secrets in here
.env*
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ environment which supports [dev containers][devcontainers].
git clone https://github.com/iainlane/coldoutsi.de.git
```

2. Open the project in the IDE
2. If using AWS SSO and a non-default profile and/or region, create a file
`.devcontainer/.env` and set `AWS_PROFILE` and/or `AWS_REGION` accordingly.

3. If using VS Code, click "Reopen in Container" when prompted. This will build
3. Open the project in the IDE

4. If using VS Code, click "Reopen in Container" when prompted. This will build
and start the container with all the necessary dependencies installed.

[devcontainers]: https://containers.dev/
Expand Down
6 changes: 6 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export default tseslint.config(
"unicorn/no-typeof-undefined": "error",
},
},
{
files: ["pulumi/**/*.ts"],
rules: {
"@typescript-eslint/no-unused-vars": "off",
},
},
{
files: ["**/*.test.ts"],
...jestPlugin.configs["flat/recommended"],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"workspaces": {
"packages": [
"app",
"gen/*"
"gen/*",
"pulumi"
]
},
"type": "module",
Expand Down
2 changes: 2 additions & 0 deletions pulumi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
node_modules/
6 changes: 6 additions & 0 deletions pulumi/Pulumi.dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
secretsprovider: awskms://12e131e6-2150-4361-913b-803c04bd5ed5?region=eu-west-2&awssdk=v2
encryptedkey: AQICAHhLE3kXzgyhKhfd8kMt7I2EBNdrJw7DPra9AQz3o1duvwFP5X23eeRpxqRKtjmP4VNoAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMDGX3lZXomIJHHVIMAgEQgDuk/UkIZt7A+8db7RGO9aWvexlDZxGmSK6m7Wda/LXX0gblOSKbjyYxW+cheqvz0Jvx8fkHNPep0dZgSA==
config:
gitHubRepo: iainlane/coldoutsi.de
targetDomain: dev
targetZone: coldoutsi.de
11 changes: 11 additions & 0 deletions pulumi/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: coldoutsi.de
runtime:
name: nodejs
options:
# https://github.com/TypeStrong/ts-node/issues/1007
nodeargs: "--loader ts-node/esm --no-warnings"
description: Pulumi program for coldoutsi.de
config:
pulumi:tags:
value:
pulumi:template: typescript
155 changes: 155 additions & 0 deletions pulumi/cloudfront.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as aws from "@pulumi/aws";
import * as aws_native from "@pulumi/aws-native";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();

const targetZone = config.require("targetZone");
const targetDomain = config.require("targetDomain");

const targetDomainFull = `${targetDomain}.${targetZone}`;

const hostedZone = new aws.route53.Zone("zone", {
name: targetZone,
});

const logsBucket = new aws_native.s3.Bucket("requestLogs", {
bucketName: `${targetDomainFull}-logs`,
ownershipControls: {
rules: [
{
objectOwnership:
aws_native.s3.BucketOwnershipControlsRuleObjectOwnership
.BucketOwnerPreferred,
},
],
},
publicAccessBlockConfiguration: {
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
},
});

const awsUsEast = new aws.Provider("aws-us-east-1", {
profile: aws_native.config.profile,
region: "us-east-1",
});

const certificate = new aws.acm.Certificate(
`${targetDomain}-cert`,
{
domainName: targetDomainFull,
validationMethod: "DNS",
},
{
provider: awsUsEast,
},
);

const certificateValidationDomain = new aws.route53.Record(
`${targetDomain}-cert-validation`,
{
name: certificate.domainValidationOptions[0].resourceRecordName,
zoneId: hostedZone.zoneId,
type: certificate.domainValidationOptions[0].resourceRecordType,
records: [certificate.domainValidationOptions[0].resourceRecordValue],
ttl: 60,
},
);

const certificateValidation = new aws.acm.CertificateValidation(
"certificateValidation",
{
certificateArn: certificate.arn,
validationRecordFqdns: [certificateValidationDomain.fqdn],
},
{
provider: awsUsEast,
},
);

const origin = {
customOriginConfig: {
originProtocolPolicy: "https-only",
},
domainName: `api.${targetDomainFull}`,
id: `api-${targetDomainFull}`,
} satisfies aws_native.types.input.cloudfront.DistributionOriginArgs;

const cachePolicy = new aws_native.cloudfront.CachePolicy(
"coldoutsi.de-cache-policy",
{
cachePolicyConfig: {
name: "coldoutsi-de-cache-policy",
defaultTtl: 60 * 60, // 1 hour
minTtl: 60, // 1 minute
maxTtl: 60 * 60 * 24, // 1 day
parametersInCacheKeyAndForwardedToOrigin: {
cookiesConfig: {
cookieBehavior: "none",
},
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
headersConfig: {
headerBehavior: "whitelist",
headers: [
"Accept",
"Accept-Language",
"CloudFront-Viewer-Latitude",
"CloudFront-Viewer-Longitude",
"Content-Type",
"If-None-Match",
"Last-Modified",
],
},
queryStringsConfig: {
queryStringBehavior: "whitelist",
queryStrings: ["format"],
},
},
},
},
);

const cloudFrontDistribution = new aws_native.cloudfront.Distribution(
"coldoutsi.de-dev",
{
distributionConfig: {
aliases: [targetDomainFull],
defaultCacheBehavior: {
cachePolicyId: cachePolicy.id,
targetOriginId: origin.id,
viewerProtocolPolicy: "redirect-to-https",
},
enabled: true,
httpVersion: "http3",
ipv6Enabled: true,
logging: {
bucket: logsBucket.domainName,
includeCookies: false,
},
origins: [origin],
// https://aws.amazon.com/cloudfront/pricing/
priceClass: "PriceClass_100",
viewerCertificate: {
acmCertificateArn: certificate.arn,
sslSupportMethod: "sni-only",
},
},
},
);

const aliasRecord = new aws.route53.Record(`dns-${targetDomainFull}`, {
name: targetDomainFull,
zoneId: hostedZone.zoneId,
type: "A",
aliases: [
{
evaluateTargetHealth: true,
name: cloudFrontDistribution.domainName,
zoneId: "Z2FDTNDATAQYW2", // CloudFront zone ID
},
],
});
3 changes: 3 additions & 0 deletions pulumi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// TODO: why do we have to give the extension here?
import "./cloudfront.ts";
export * from "./oidc.ts";
Loading

0 comments on commit 0880121

Please sign in to comment.