Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production deployment #646

Merged
merged 11 commits into from
May 9, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/build-email-function.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_call:

jobs:
plan:
build-email-function:
runs-on: ubuntu-22.04

steps:
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/terraform-apply.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: terraform apply

on:
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
default: "test"
type: choice
options:
- test

jobs:
plan:
uses: ./.github/workflows/terraform-plan.yml
with:
environment: ${{ inputs.environment }}
workflow_call: true
secrets: inherit

apply:
runs-on: ubuntu-22.04

# Related
# - https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
# - https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps
permissions:
id-token: write # Required for aws-actions/configure-aws-credentials
contents: read # Required for aws-actions/configure-aws-credentials

needs: [plan]

environment: "terraform-${{ inputs.environment }}"

steps:
- uses: actions/[email protected]

- name: Read .terraform-version
id: terraform_version
run: echo "value=$(cat .terraform-version)" >> $GITHUB_OUTPUT
working-directory: terraform

- uses: hashicorp/[email protected]
with:
terraform_version: "${{ steps.terraform_version.outputs.value }}"

- name: Configure AWS Credentials
uses: aws-actions/[email protected]
with:
role-to-assume: ${{ vars.IAM_ROLE_ARN }}
aws-region: eu-central-1

- name: Retrieve email-function artifact
uses: actions/[email protected]
with:
name: email-function-dist
path: bun-packages/packages/email-function/dist

- name: Retrieve tfplan artifact
uses: actions/[email protected]
with:
name: tfplan
path: terraform/

- name: terraform init
run: terraform init -backend-config=${{ inputs.environment }}.s3.tfbackend
working-directory: terraform

- name: terraform apply
run: terraform apply tfplan
working-directory: terraform
env:
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_api_subdomain: ${{ vars.API_SUBDOMAIN }}
TF_VAR_email_function_parameters: ${{ secrets.EMAIL_FUNCTION_PARAMETERS }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
42 changes: 37 additions & 5 deletions .github/workflows/terraform-plan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ on:
- terraform/**
- bun-packages/**
- cloudformation/**
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
default: "test"
type: choice
options:
- test
workflow_call:
inputs:
environment:
required: true
default: "test"
type: string

# `github.event_name` is `workflow_dispatch` even for `workflow_call`.
# https://github.com/actions/runner/discussions/1884
workflow_call:
required: false
type: boolean

jobs:
build_email_function:
Expand All @@ -29,6 +50,8 @@ jobs:

needs: [build_email_function]

environment: terraform-${{ inputs.environment || 'test' }}

steps:
- uses: actions/[email protected]

Expand All @@ -44,7 +67,7 @@ jobs:
- name: Configure AWS Credentials
uses: aws-actions/[email protected]
with:
role-to-assume: arn:aws:iam::220746603587:role/github-actions
role-to-assume: ${{ vars.IAM_ROLE_ARN }}
aws-region: eu-central-1

- name: Retrieve email-function artifact
Expand All @@ -54,17 +77,18 @@ jobs:
path: bun-packages/packages/email-function/dist

- name: terraform init
run: terraform init -backend-config=test.s3.tfbackend
run: terraform init -backend-config=${{ inputs.environment || 'test' }}.s3.tfbackend
working-directory: terraform

- name: terraform plan
run: terraform plan -no-color
run: terraform plan -no-color -out=tfplan
working-directory: terraform
id: plan
continue-on-error: true
env:
TF_VAR_cloudflare_api_token: ${{ secrets.TF_VAR_CLOUDFLARE_API_TOKEN }}
TF_VAR_api_subdomain: sinister-api-test
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_api_subdomain: ${{ vars.API_SUBDOMAIN }}
TF_VAR_email_function_parameters: ${{ secrets.EMAIL_FUNCTION_PARAMETERS }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}

- uses: actions/[email protected]
Expand Down Expand Up @@ -116,3 +140,11 @@ jobs:
- name: Break on terraform plan failure
if: steps.plan.outcome == 'failure'
run: exit 1

- name: Store artifact
uses: actions/[email protected]
if: inputs.workflow_call
with:
name: tfplan
path: terraform/tfplan
retention-days: 1
Binary file modified bun-packages/bun.lockb
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const emailConfirmation = async (
}

await mg.messages.create("mailgun.simonknittel.de", {
from: "Sinister Incorporated <noreply@mailgun.simonknittel.de>",
from: "Sinister Incorporated <no-reply@mailgun.simonknittel.de>",
to: htmlMessages.map((message) => message.to),
subject: subject,
html: htmlEmail,
Expand All @@ -53,7 +53,7 @@ export const emailConfirmation = async (
// }

// await mg.messages.create("mailgun.simonknittel.de", {
// from: "Sinister Incorporated <noreply@mailgun.simonknittel.de>",
// from: "Sinister Incorporated <no-reply@mailgun.simonknittel.de>",
// to: textMessages.map((message) => message.to),
// subject: subject,
// text: textEmail,
Expand All @@ -62,7 +62,7 @@ export const emailConfirmation = async (
};

const subject =
"E-Mail-Adresse und Datenschutzerklärung bestätigen | Sinister Incorporated";
"E-Mail-Adresse und Datenschutzerklärung bestätigen | S.A.M. - Sinister Incorporated";

const renderHtmlEmail = (templateProps: EmailConfirmationProps) => {
return render(Email(templateProps));
Expand All @@ -71,7 +71,7 @@ const renderHtmlEmail = (templateProps: EmailConfirmationProps) => {
const renderTextEmail = (templateProps: EmailConfirmationProps) => {
const { baseUrl, host, token } = templateProps;

return `E-Mail-Adresse und Datenschutzerklärung bestätigen - Sinister Inc
return `E-Mail-Adresse und Datenschutzerklärung bestätigen - S.A.M.

Deine E-Mail-Adresse und die Datenschutzerklärung müssen bestätigt werden bevor du ${host} nutzen kannst.

Expand Down
2 changes: 1 addition & 1 deletion bun-packages/packages/emails/emails/EmailConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function Email({
<Body className="bg-neutral-800 text-neutral-50 font-sans px-6 pt-2 pb-6">
<Container className="mx-auto w-[480px]">
<Text className="text-center text-5xl text-sinister-red-500 font-extrabold uppercase">
Sinister Inc
S.A.M.
</Text>
<Text className="font-bold">
Deine E-Mail-Adresse und die <Link href={`${baseUrl}/api/confirm-email?token=${token}`} className="text-sinister-red-500">Datenschutzerklärung</Link> müssen bestätigt werden bevor du{" "}
Expand Down
2 changes: 2 additions & 0 deletions cloudformation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test-parameters.json
prod-parameters.json
28 changes: 21 additions & 7 deletions cloudformation/setup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ AWSTemplateFormatVersion: 2010-09-09

Description: Initial setup for Terraform and GitHub Actions

Parameters:
TerraformBackendBucketName:
Type: String
Description: The name of the S3 bucket to use as the Terraform backend

Resources:
TerraformBackendBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: terraform-backend-aowb47tawo48wvt4
BucketName: !Ref TerraformBackendBucketName
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
Expand Down Expand Up @@ -37,9 +42,13 @@ Resources:
ClientIdList:
- sts.amazonaws.com
ThumbprintList:
# The following thumbprints aren't required as AWS writes: "AWS secures communication with this OIDC identity provider (IdP) using our library of trusted CAs rather than using a certificate thumbprint to verify the server certificate of your IdP. Your legacy thumbprint(s) will remain in your configuration but will no longer be needed for validation."
# The following thumbprints aren't required as AWS writes: "AWS secures communication with this
# OIDC identity provider (IdP) using our library of trusted CAs rather than using a certificate
# thumbprint to verify the server certificate of your IdP. Your legacy thumbprint(s) will remain
# in your configuration but will no longer be needed for validation."
#
# Source for the thumbprints: https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/
# Source for the thumbprints:
# https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/
- 6938fd4d98bab03faadb97b34396831e3780aea1
- 1c58a3a8518e8759bf075b76b750d4f2df264fcd

Expand All @@ -58,12 +67,10 @@ Resources:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
StringLike:
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-token-claims
token.actions.githubusercontent.com:sub:
- repo:simonknittel/sinister-incorporated:job_workflow_ref:simonknittel/sinister-incorporated/.github/workflows/terraform-plan.yml@refs/pull/*
- repo:simonknittel/sinister-incorporated:job_workflow_ref:simonknittel/sinister-incorporated/.github/workflows/terraform-apply.yml@refs/heads/main
- repo:simonknittel/sinister-incorporated:*
Policies:
- PolicyName: terraform
- PolicyName: terraform-plan
PolicyDocument:
Version: 2012-10-17
Statement:
Expand Down Expand Up @@ -143,6 +150,13 @@ Resources:
- ssm:GetParameter
- sts:GetCallerIdentity
Resource: "*"
- PolicyName: terraform-apply
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "*" # TODO: Don't do this
Resource: "*"

Outputs:
TerraformBackendBucketName:
Expand Down
33 changes: 15 additions & 18 deletions docs/setup-test-and-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@

## 3. Set up GitHub

1. `gh auth login`
2. `gh api --method PUT -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/simonknittel/sinister-incorporated/actions/oidc/customization/sub -F use_default=false -f "include_claim_keys[]=repo" -f "include_claim_keys[]=job_workflow_ref"`
1. `gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/simonknittel/sinister-incorporated/actions/oidc/customization/sub`
3. Enable "Allow GitHub Actions to create and approve pull requests" in Settings/Actions/General/Workflow permissions

### Related

- https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-token-claims
- https://docs.github.com/en/rest/actions/oidc?apiVersion=2022-11-28#set-the-customization-template-for-an-oidc-subject-claim-for-a-repository
1. Create environments
1. `terraform-test`
2. `terraform-prod`
2. Create environment secrets
- `CLOUDFLARE_API_TOKEN`
- `EMAIL_FUNCTION_PARAMETERS`
- `[{"name":"...","value":"..."}]`
3. Create environment variables
- `API_SUBDOMAIN`
- `IAM_ROLE`
4. Enable "Allow GitHub Actions to create and approve pull requests" in Settings/Actions/General/Workflow permissions

## 4. Set up AWS

1. Create two AWS accounts
1. Create AWS accounts

1. `sinister-incorporated-test`
2. `sinister-incorporated-prod`
Expand Down Expand Up @@ -51,16 +53,11 @@

3. Create and deploy setup stack with AWS CloudFormation

1. Create and populate `test-parameters.json` and `prod-parameters.json`
1. `AWS_PROFILE=sinister-incorporated-test aws sso login`
2. `AWS_PROFILE=sinister-incorporated-test aws --region eu-central-1 cloudformation deploy --template-file ./cloudformation/setup.yaml --stack-name setup --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM --tags ManagedBy=CloudFormation Repository=simonknittel/sinister-incorporated CloudFormationStack=setup`

4. Create parameters in AWS System Manager (make sure to replace `foobar` with the actual values)

1. `AWS_PROFILE=sinister-incorporated-test aws sso login`
2. `AWS_PROFILE=sinister-incorporated-test aws --region eu-central-1 ssm put-parameter --name /email-function/mailgun-api-key --value foobar --type SecureString --overwrite`
3. `AWS_PROFILE=sinister-incorporated-test aws --region eu-central-1 ssm put-parameter --name /email-function/api-key --value foobar --type SecureString --overwrite`
2. `AWS_PROFILE=sinister-incorporated-test aws --region eu-central-1 cloudformation deploy --template-file setup.yaml --stack-name setup --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM --tags ManagedBy=CloudFormation Repository=simonknittel/sinister-incorporated --parameter-override file://test-parameters.json`

5. Manually set up AWS User Notifications through the console
4. Manually set up AWS User Notifications through the console

1. Notification hubs: eu-central-1
2. Create notification configuration for CloudWatch
Expand Down
5 changes: 1 addition & 4 deletions terraform/email-function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,5 @@ module "email_function" {
event_bus = aws_cloudwatch_event_bus.api_gateway
event_bus_detail_type = "EmailRequested"
dynamodb = aws_dynamodb_table.api_gateway_processed_requests

parameter_store = [
"/mailgun-api-key"
]
parameters = var.email_function_parameters
}
10 changes: 6 additions & 4 deletions terraform/modules/api-gateway-lambda/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ resource "aws_iam_role" "main" {
[
data.aws_kms_alias.ssm.target_key_arn,
],
tolist(data.aws_ssm_parameter.custom[*].arn),
tolist(aws_ssm_parameter.custom[*].arn),
)
},
]
Expand All @@ -51,7 +51,9 @@ data "aws_kms_alias" "ssm" {
name = "alias/aws/ssm"
}

data "aws_ssm_parameter" "custom" {
count = length(var.parameter_store)
name = "/${var.function_name}${var.parameter_store[count.index]}"
resource "aws_ssm_parameter" "custom" {
count = length(var.parameters)
type = "SecureString"
name = "/${var.function_name}/${var.parameters[count.index].name}"
value = var.parameters[count.index].value
}
7 changes: 5 additions & 2 deletions terraform/modules/api-gateway-lambda/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ variable "method" {
type = string
}

variable "parameter_store" {
type = list(string)
variable "parameters" {
type = list(object({
name = string
value = string
}))
default = []
}

Expand Down
10 changes: 6 additions & 4 deletions terraform/modules/eventbridge-sqs-lambda/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ resource "aws_iam_role" "main" {
[
data.aws_kms_alias.ssm.target_key_arn,
],
tolist(data.aws_ssm_parameter.custom[*].arn),
tolist(aws_ssm_parameter.custom[*].arn),
)
},
{
Expand Down Expand Up @@ -73,7 +73,9 @@ data "aws_kms_alias" "ssm" {
name = "alias/aws/ssm"
}

data "aws_ssm_parameter" "custom" {
count = length(var.parameter_store)
name = "/${var.function_name}${var.parameter_store[count.index]}"
resource "aws_ssm_parameter" "custom" {
count = length(var.parameters)
type = "SecureString"
name = "/${var.function_name}/${var.parameters[count.index].name}"
value = var.parameters[count.index].value
}
Loading