Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,18 @@ Then a deployment can be made with `cdk`:
ci_role="$(aws iam list-roles | jq --raw-output '.Roles[] | select(.RoleName | contains("CiTopo")) | select(.RoleName | contains("-CiRole")).Arn')"
admin_role="arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/AccountAdminRole"
workflow_maintainer_role="$(aws cloudformation describe-stacks --stack-name=TopographicSharedResourcesProd | jq --raw-output .Stacks[0].Outputs[0].OutputValue)"

npx cdk deploy --context=maintainer-arns="${ci_role},${admin_role},${workflow_maintainer_role}" Workflows
```

### Testing Clusters

Sometimes a simpler cluster can be useful for testing, some deployment steps can be turned off

- `cluster-suffix` - EKS cluster names are unique, use a specific cluster
- `no-database` - Turn off creating RDS database
- `no-slack` - Turn off creating slack monitoring alerts for RDS

### Deploy CDK8s

_CDK8s is used to manage Kubernetes resources on the cluster previously created._
Expand Down
29 changes: 21 additions & 8 deletions infra/cdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { App } from 'aws-cdk-lib';

import { ClusterName, DefaultRegion } from './constants.js';
import { DefaultRegion, getClusterName } from './constants.js';
import { tryGetContextArns } from './eks/arn.js';
import { LinzEksCluster } from './eks/cluster.js';
import { fetchSsmParameters } from './util/ssm.js';
Expand All @@ -9,24 +9,37 @@ const app = new App();

async function main(): Promise<void> {
const accountId = (app.node.tryGetContext('aws-account-id') as unknown) ?? process.env['CDK_DEFAULT_ACCOUNT'];
const maintainerRoleArns = tryGetContextArns(app.node, 'maintainer-arns');
const maintainerRoleArns = tryGetContextArns(app.node, 'maintainer-arns') ?? [];
const noDatabase = app.node.tryGetContext('no-database') === 'true';
const clusterName = getClusterName(app.node.tryGetContext('cluster-suffix'));

const slackSsmConfig = await fetchSsmParameters({
slackChannelConfigurationName: '/rds/alerts/slack/channel/name',
slackWorkspaceId: '/rds/alerts/slack/workspace/id',
slackChannelId: '/rds/alerts/slack/channel/id',
});

if (maintainerRoleArns == null) throw new Error('Missing context: maintainer-arns');
if (typeof accountId !== 'string') {
throw new Error("Missing AWS Account information, set with either '-c aws-account-id' or $CDK_DEFAULT_ACCOUNT");
throw new Error(
"Missing AWS Account information, set with either '--context=aws-account-id=123456789' or $CDK_DEFAULT_ACCOUNT",
);
}

new LinzEksCluster(app, ClusterName, {
if (maintainerRoleArns.length === 0) console.log('No maintainer roles found');

/** LINZ conventional name for Argo Workflows artifact bucket */
const ScratchBucketName = `linz-${clusterName.toLowerCase()}-scratch`;

new LinzEksCluster(app, clusterName, {
env: { region: DefaultRegion, account: accountId },
maintainerRoleArns,
slackChannelConfigurationName: slackSsmConfig.slackChannelConfigurationName,
slackWorkspaceId: slackSsmConfig.slackWorkspaceId,
slackChannelId: slackSsmConfig.slackChannelId,
slack: {
workspaceId: slackSsmConfig.slackWorkspaceId,
channelId: slackSsmConfig.slackChannelId,
channelConfigurationName: slackSsmConfig.slackChannelConfigurationName,
},
tempBucketName: ScratchBucketName,
argoDatabaseName: noDatabase ? undefined : 'argo',
});

app.synth();
Expand Down
24 changes: 15 additions & 9 deletions infra/cdk8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import { FluentBit } from './charts/fluentbit.js';
import { Karpenter, KarpenterProvisioner } from './charts/karpenter.js';
import { CoreDns } from './charts/kube-system.coredns.js';
import { NodeLocalDns } from './charts/kube-system.node.local.dns.js';
import { CfnOutputKeys, ClusterName, ScratchBucketName, UseNodeLocalDns, validateKeys } from './constants.js';
import { CfnOutputKeys, getClusterName, validateKeys } from './constants.js';
import { describeCluster, getCfnOutputs } from './util/cloud.formation.js';
import { fetchSsmParameters } from './util/ssm.js';

const app = new App();

async function main(): Promise<void> {
const noNodeLocalDns = app.node.tryGetContext('no-node-local-dns') === 'true';
const clusterName = getClusterName(app.node.tryGetContext('cluster-suffix'));

// Get cloudformation outputs
const [cfnOutputs, ssmConfig, clusterConfig] = await Promise.all([
getCfnOutputs(ClusterName),
getCfnOutputs(clusterName),
fetchSsmParameters({
// Config for Cloudflared to access argo-server
tunnelId: '/eks/cloudflared/argo/tunnelId',
Expand All @@ -31,15 +34,16 @@ async function main(): Promise<void> {
// Argo Database connection password
argoDbPassword: '/eks/argo/postgres/password',
}),
describeCluster(ClusterName),
describeCluster(clusterName),
]);
validateKeys(cfnOutputs);

const coredns = new CoreDns(app, 'dns', {});

// Node localDNS is very expermential in this cluster, it can and will break DNS resolution
// If there are any issues with DNS, NodeLocalDNS should be disabled first.
if (UseNodeLocalDns) {
// `--context=no-node-local-dns=true`
if (noNodeLocalDns === false) {
const ipv6Cidr = clusterConfig.kubernetesNetworkConfig?.serviceIpv6Cidr;
if (ipv6Cidr == null) throw new Error('Unable to use node-local-dns without ipv6Cidr');
const nodeLocal = new NodeLocalDns(app, 'node-local-dns', { serviceIpv6Cidr: ipv6Cidr });
Expand All @@ -48,20 +52,20 @@ async function main(): Promise<void> {

const fluentbit = new FluentBit(app, 'fluentbit', {
saName: cfnOutputs[CfnOutputKeys.FluentBitServiceAccountName],
clusterName: ClusterName,
clusterName: clusterName,
});
fluentbit.addDependency(coredns);

const karpenter = new Karpenter(app, 'karpenter', {
clusterName: ClusterName,
clusterName: clusterName,
clusterEndpoint: cfnOutputs[CfnOutputKeys.ClusterEndpoint],
saName: cfnOutputs[CfnOutputKeys.KarpenterServiceAccountName],
saRoleArn: cfnOutputs[CfnOutputKeys.KarpenterServiceAccountRoleArn],
instanceProfile: cfnOutputs[CfnOutputKeys.KarpenterDefaultInstanceProfile],
});

const karpenterProvisioner = new KarpenterProvisioner(app, 'karpenter-provisioner', {
clusterName: ClusterName,
clusterName: clusterName,
clusterEndpoint: cfnOutputs[CfnOutputKeys.ClusterEndpoint],
saName: cfnOutputs[CfnOutputKeys.KarpenterServiceAccountName],
saRoleArn: cfnOutputs[CfnOutputKeys.KarpenterServiceAccountRoleArn],
Expand All @@ -72,9 +76,11 @@ async function main(): Promise<void> {

new ArgoWorkflows(app, 'argo-workflows', {
namespace: 'argo',
clusterName: ClusterName,
clusterName: clusterName,
saName: cfnOutputs[CfnOutputKeys.ArgoRunnerServiceAccountName],
tempBucketName: ScratchBucketName,
tempBucketName: cfnOutputs[CfnOutputKeys.ScratchBucketName],
argoDbUsername: cfnOutputs[CfnOutputKeys.ArgoDbUsername],
argoDbName: cfnOutputs[CfnOutputKeys.ArgoDbName],
argoDbEndpoint: cfnOutputs[CfnOutputKeys.ArgoDbEndpoint],
argoDbPassword: ssmConfig.argoDbPassword,
});
Expand Down
69 changes: 45 additions & 24 deletions infra/charts/argo.workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Chart, ChartProps, Duration, Helm } from 'cdk8s';
import { Secret } from 'cdk8s-plus-30';
import { Construct } from 'constructs';

import { ArgoDbName, ArgoDbUser, DefaultRegion } from '../constants.js';
import { DefaultRegion } from '../constants.js';
import { applyDefaultLabels } from '../util/labels.js';

export interface ArgoWorkflowsProps {
Expand All @@ -12,30 +12,48 @@ export interface ArgoWorkflowsProps {
* @example "linz-workflows-scratch"
*/
tempBucketName: string;

/**
* Name of the Service Account used to run workflows
*
* @example "workflow-runner-sa"
*/
saName: string;

/**
* Name of the EKS cluster
*
* @example "Workflows"
*/
clusterName: string;

/**
* The Argo database endpoint
*
* @example "argodb-argodb4be14fa2-p8yjinijwbro.cmpyjhgv78aj.ap-southeast-2.rds.amazonaws.com"
*/
argoDbEndpoint: string;
argoDbEndpoint?: string;

/**
* The Argo database password
*
* @example "eighoo5room0aeM^ahz0Otoh4aakiipo"
*/
argoDbPassword: string;

/**
* Username to connect to the database
*
* @example "argo_user"
*/
argoDbUsername: string;

/**
* Database name
*
* @example "argo"
*/
argoDbName: string;
}

/**
Expand Down Expand Up @@ -72,29 +90,32 @@ export class ArgoWorkflows extends Chart {
},
};

const argoDbSecret = new Secret(this, 'argo-postgres-config', {});
argoDbSecret.addStringData('username', ArgoDbUser);
argoDbSecret.addStringData('password', props.argoDbPassword);
let persistence = undefined;
if (props.argoDbEndpoint) {
const argoDbSecret = new Secret(this, 'argo-postgres-config', {});
argoDbSecret.addStringData('username', props.argoDbUsername);
argoDbSecret.addStringData('password', props.argoDbPassword);

const persistence = {
connectionPool: {
maxIdleConns: 100,
maxOpenConns: 0,
},
nodeStatusOffLoad: true,
archive: true,
archiveTTL: '', // never expire archived workflows
postgresql: {
host: props.argoDbEndpoint,
port: 5432,
database: ArgoDbName,
tableName: 'argo_workflows',
userNameSecret: { name: argoDbSecret.name, key: 'username' },
passwordSecret: { name: argoDbSecret.name, key: 'password' },
ssl: true,
sslMode: 'require',
},
};
persistence = {
connectionPool: {
maxIdleConns: 100,
maxOpenConns: 0,
},
nodeStatusOffLoad: true,
archive: true,
archiveTTL: '', // never expire archived workflows
postgresql: {
host: props.argoDbEndpoint,
port: 5432,
database: props.argoDbName,
tableName: 'argo_workflows',
userNameSecret: { name: argoDbSecret.name, key: 'username' },
passwordSecret: { name: argoDbSecret.name, key: 'password' },
ssl: true,
sslMode: 'require',
},
};
}

const DefaultNodeSelector = {
'eks.amazonaws.com/capacityType': 'ON_DEMAND',
Expand Down
30 changes: 13 additions & 17 deletions infra/constants.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
/** Cluster name */
export const ClusterName = 'Workflows';
/** LINZ conventional name for Argo Workflows artifact bucket */
export const ScratchBucketName = `linz-${ClusterName.toLowerCase()}-scratch`;
/** Argo Database Instance name */
export const ArgoDbInstanceName = 'ArgoDb';
/** Argo Database name */
export const ArgoDbName = 'argo';
/** Argo Database user */
export const ArgoDbUser = 'argo_user';
/** AWS default region for our stack */
export const DefaultRegion = 'ap-southeast-2';

const BaseClusterName = 'Workflows';
/**
* Should NodeLocal DNS be enabled for the cluster
*
* @see ./charts/kube-system.coredns.ts
* Using the context generate a cluster name
* @returns cluster name of `Workflows`
*/
export const UseNodeLocalDns = true;
export function getClusterName(clusterSuffix: unknown): string {
if (typeof clusterSuffix !== 'string') return BaseClusterName;
// convert `blacha` into `WorkflowsBlacha`
return `Workflows` + clusterSuffix.slice(0, 1).toUpperCase() + clusterSuffix.slice(1).toLowerCase();
}

/** CloudFormation Output to access from CDK8s */
export const CfnOutputKeys = {
ClusterEndpoint: 'ClusterEndpoint',

ScratchBucketName: 'ScratchBucketName',

ArgoDbEndpoint: 'ArgoDbEndpoint',
ArgoDbName: 'ArgoDbName',
ArgoDbUsername: 'ArgoDbUsername',

KarpenterServiceAccountName: 'KarpenterServiceAccountName',
KarpenterServiceAccountRoleArn: 'KarpenterServiceAccountRoleArn',
Expand All @@ -45,7 +43,5 @@ export type CfnOutputMap = Record<ICfnOutputKeys, string>;
*/
export function validateKeys(cfnOutputs: Record<string, string>): asserts cfnOutputs is CfnOutputMap {
const missingKeys = Object.values(CfnOutputKeys).filter((f) => cfnOutputs[f] == null);
if (missingKeys.length > 0) {
throw new Error(`Missing CloudFormation Outputs for keys ${missingKeys.join(', ')}`);
}
if (missingKeys.length > 0) throw new Error(`Missing CloudFormation Outputs for keys ${missingKeys.join(', ')}`);
}
Loading