From a7a57e850d1caa5a3c4579a301e4ac4ef137d6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kj=C3=A6r?= Date: Sat, 7 Oct 2023 20:53:58 +0200 Subject: [PATCH] Update css, resize images, add content to part 2 --- posts/2023-10-07-the-stack-part-1.md | 10 +- posts/2023-10-07-the-stack-part-2.md | 195 ++++++++++++++++++++++++++- resources/scss/_post.scss | 1 + 3 files changed, 197 insertions(+), 9 deletions(-) diff --git a/posts/2023-10-07-the-stack-part-1.md b/posts/2023-10-07-the-stack-part-1.md index f547b05..e93521b 100644 --- a/posts/2023-10-07-the-stack-part-1.md +++ b/posts/2023-10-07-the-stack-part-1.md @@ -1,5 +1,5 @@ --- -title: "\"The Stack\" Part 1: Setting up your AWS Account Structure" +title: "The Stack Part 1: Setting up your AWS Account Structure" tags: aws, cloud, infrastructure --- @@ -119,7 +119,7 @@ We'll immediately start our good habit of adding billing tags whereever we can,
👈 Open the toggle for an example -KMS Configuration +KMS Configuration
@@ -127,7 +127,7 @@ Skip through Step 3, Step 4, and click "Finish" on the review step. Once the key is created we'll immediately edit it now that it has gotten a Key ID. If we don't we'll run into this nice error later on: -KMS error +KMS error 1. Click on your newly created key. 2. Note down the Key ID (e.g. `12345678-1234-1234-1234-123456789012`). @@ -224,7 +224,7 @@ Find the User portal URL: 3. Copy the **User portal URL** from the **Federated access management** card. 4. Bookmark this URL, you're gonna need it a lot. -User portal URL +User portal URL Once you're logged into the Control Tower account using the portal, jump into the Account Factory again: @@ -249,7 +249,7 @@ For each account, in the **Create account** process, fill in: Pick the appropriate Organizational unit according to the list above, and click **Create account**. You should end up with an overall structure like the following: -Account overview +Account overview # Next Steps diff --git a/posts/2023-10-07-the-stack-part-2.md b/posts/2023-10-07-the-stack-part-2.md index 975f01c..05d30e8 100644 --- a/posts/2023-10-07-the-stack-part-2.md +++ b/posts/2023-10-07-the-stack-part-2.md @@ -1,5 +1,5 @@ --- -title: "\"The Stack\" Part 2: Automating Deployments via CI" +title: "The Stack Part 2: Automating Deployments via CI" tags: aws, cloud, infrastructure, cdk --- @@ -66,18 +66,205 @@ Repeat those steps with the relevant values for each of the environments `Integr Your `Environment` overview will look like this: -Overview of Environments +Overview of Environments And each environment will roughly look like this: -Configuration, secrets, and variables of an environment +Configuration, secrets, and variables of an environment # CDK: Infrastructure as Code [CDK](https://github.com/aws/aws-cdk) is our tool of choice for Infrastructure as Code. We'll start from the default template and adjust it to use [Bun](https://bun.sh/) which simplifies the process of running CDK commands while using TypeScript. -Instead of setting this up from scratch, start from the template for this step in the [GitHub repository](https://github.com/codetalkio/the-stack/tree/part-2-automatic-deployments). +Instead of setting this up from scratch, start from the template for this step in the [GitHub repository](https://github.com/codetalkio/the-stack/tree/part-2-automatic-deployments). We'll go through what this contains in the next two sections. + +## Explanation: CDK Stack + +We structure our CDK stack as follows: +- `deployment/`: The root folder for all things CDK + - `bin/`: The entry point for all our stacks + - `deployment.ts`: Our "executable" that CDK run run (defined in `cdk.json`) + - `helpers.ts`: Helper functions to make our CDK code more readable and safe to run + - `lib/`: The actual logic of our CDK stacks + - `base/`: Our base layer containing all the resources that are shared across all stacks + - `stack.ts`: Gathers all of our base stacks into one stack + - `domain.ts`: Sets up our Hosted Zone and ACM certificates + +This might seem like overkill right now, but will benefit us quite quickly as we start adding more stacks to our project. + +In `deployment.ts` you'll see the root of our CDK stack: + +```typescript +// ...imports +const app = new cdk.App(); + +/** + * Define our base stack that provisions the infrastructure for our application, such + * as domain names, certificates, and other resources that are shared across all. + */ +const baseStackName = "Base"; +if (matchesStack(app, baseStackName)) { + new BaseStack(app, baseStackName, { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT || process.env.AWS_ACCOUNT_ID, + region: process.env.CDK_DEFAULT_REGION || process.env.AWS_REGION, + }, + domain: validateEnv("DOMAIN", baseStackName), + }); +} +``` + +We've set up some conveniences to easily run a single stack, via `matchesStack`, and to validate our environment variables, via `validateEnv`. + +Our `BaseStack` is then defined in `lib/base/stack.ts`, and more or less just pieces together the types and the sub-stacks in the `base/` directory: + +```typescript +// ...imports +interface StackProps extends cdk.StackProps, domain.StackProps {} + +export class Stack extends cdk.Stack { + constructor(scope: Construct, id: string, props: StackProps) { + super(scope, id, props); + + // Set up our domain stack. + new domain.Stack(this, "Domain", props); + } +} +``` + +And finally, to the meat of things, our `lib/base/domain.ts` is currently where the magic happens: + +```typescript +// ...imports +export interface StackProps extends cdk.StackProps { + /** + * The domain name the application is hosted under. + */ + readonly domain: string; +} + +/** + * Set up a Hosted Zone to manage our domain names. + * + * https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53.HostedZone.html + */ +export class Stack extends cdk.Stack { + constructor(scope: Construct, id: string, props: StackProps) { + super(scope, id, props); + + // Set up the hosted zone. + const hostedZone = new route53.HostedZone(this, "HostedZone", { + zoneName: props.domain, + }); + + // Set up an ACM certificate for the domain, and validate it using DNS. + new acm.Certificate(this, "Certificate", { + domainName: props.domain, + validation: acm.CertificateValidation.fromDns(hostedZone), + }); + } +} +``` + +This sets up a Hosted Zone and an ACM certificate for our domain, and configures it to validate the Certificate via DNS validation. + +## Explanation: Automated Deployments via GitHub Actions + +We have two workflows to deploy things, they share much of the same logic, so let's focus on the commonalities first. + +Both of them do a few things: +- Sets up a trigger on `workflow_dispatch` so that we can manually trigger the workflow. +- Set up a matrix of environments to deploy to. +- Configures and `environment` and a `concurrency` group. The `concurrency` group is important, since we don't want to deploy to the same environment at the same time. +- Installs `bun` and our dependencies. + +All of this looks like this, taken from the `cd-bootstrap` workflow: + +```yaml +name: "Deployment: Bootstrap" + +on: + workflow_dispatch: + +defaults: + run: + shell: bash # Set the default shell to bash. + +jobs: + bootstrap: + name: bootstrap + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Add new environments to this list to run bootstrap on them when manually triggered. + environment: + - "Integration Test" + - "Production Single-tenant" + - "Production Multi-tenant" + environment: ${{ matrix.environment }} + # Limit to only one concurrent deployment per environment. + concurrency: + group: ${{ matrix.environment }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v3 + - uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + working-directory: ./deployment + run: bun install +``` + +We now diverge (beyond the naming in the above example) for each workflow. The bootstrap flow is quite simple, it just runs `bun run cdk bootstrap` for each environment: + +```yaml + - name: Bootstrap account + working-directory: ./deployment + env: + AWS_REGION: ${{ vars.AWS_REGION }} + AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: bun run cdk bootstrap +``` + +Our deployment workflow does a bit more, we also synthesize our stacks and run any tests we have: + +```yaml + - name: Synthesize the whole stack + working-directory: ./deployment + env: + DOMAIN: ${{ vars.DOMAIN }} + AWS_REGION: ${{ vars.AWS_REGION }} + AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: bun run cdk synth --all + + - name: Run tests + working-directory: ./deployment + env: + DOMAIN: ${{ vars.DOMAIN }} + AWS_REGION: ${{ vars.AWS_REGION }} + AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }} + run: bun test + + - name: Deploy to ${{ matrix.environment }} + working-directory: ./deployment + env: + DOMAIN: ${{ vars.DOMAIN }} + AWS_REGION: ${{ vars.AWS_REGION }} + AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: bun run cdk deploy --all --require-approval never +``` + +The synth and test steps ensure we have a minimum of sanity checking in place. + +## Trigger the Workflows Push your project to GitHub. You now have access to the workflows and can trigger them manually. diff --git a/resources/scss/_post.scss b/resources/scss/_post.scss index 70490a3..55d798a 100644 --- a/resources/scss/_post.scss +++ b/resources/scss/_post.scss @@ -81,6 +81,7 @@ ul, ol, blockquote, + details, pre:not(.mermaid), div.figure { @include center-content();