Skip to content

Commit

Permalink
feat: add cloudflared for remote access to argo (#213)
Browse files Browse the repository at this point in the history
#### Motivation

To provide secure external access to the Argo server import a
cloudflared tunnel

#### Modification

Imports the CDK8s configuration for cloudflared

#### Checklist

_If not applicable, provide explanation of why._

- [ ] Tests updated
- [ ] Docs updated
- [ ] Issue linked in Title
  • Loading branch information
blacha authored and amfage committed Oct 30, 2023
1 parent 9b0429d commit 4db3623
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 254 deletions.
20 changes: 20 additions & 0 deletions infra/cdk8s.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { App } from 'cdk8s';

import { ArgoSemaphore } from './charts/argo.semaphores';
import { Cloudflared } from './charts/cloudflared';
import { FluentBit } from './charts/fluentbit';
import { Karpenter, KarpenterProvisioner } from './charts/karpenter';
import { CoreDns } from './charts/kube-system.coredns';
import { CfnOutputKeys, ClusterName } from './constants';
import { getCfnOutputs } from './util/cloud.formation';
import { fetchSsmParameters } from './util/ssm';

const app = new App();

Expand All @@ -19,6 +21,17 @@ async function main(): Promise<void> {
throw new Error(`Missing CloudFormation Outputs for keys ${missingKeys.join(', ')}`);
}

const ssmConfig = await fetchSsmParameters({
// Config for Cloudflared to access argo-server
tunnelId: '/eks/cloudflared/argo/tunnelId',
tunnelSecret: '/eks/cloudflared/argo/tunnelSecret',
tunnelName: '/eks/cloudflared/argo/tunnelName',
accountId: '/eks/cloudflared/argo/accountId',

// Personal access token to gain access to linz-basemaps github user
githubPat: '/eks/github/linz-basemaps/pat',
});

new ArgoSemaphore(app, 'semaphore', {});
const coredns = new CoreDns(app, 'dns', {});
const fluentbit = new FluentBit(app, 'fluentbit', {
Expand All @@ -45,6 +58,13 @@ async function main(): Promise<void> {

karpenterProvisioner.addDependency(karpenter);

new Cloudflared(app, 'cloudflared', {
tunnelId: ssmConfig.tunnelId,
tunnelSecret: ssmConfig.tunnelSecret,
tunnelName: ssmConfig.tunnelName,
accountId: ssmConfig.accountId,
});

app.synth();
}

Expand Down
63 changes: 63 additions & 0 deletions infra/charts/cloudflared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Chart, ChartProps, Size } from 'cdk8s';
import * as kplus from 'cdk8s-plus-27';
import { Construct } from 'constructs';

import { applyDefaultLabels } from '../util/labels.js';

export class Cloudflared extends Chart {
constructor(
scope: Construct,
id: string,
props: { tunnelId: string; tunnelSecret: string; accountId: string; tunnelName: string } & ChartProps,
) {
super(scope, id, applyDefaultLabels(props, 'cloudflared', '2023.8.2', 'tunnel', 'workflows'));

// TODO should we create a new namespace every time
new kplus.Namespace(this, 'namespace', {
metadata: { name: props.namespace },
});

const cm = new kplus.ConfigMap(this, 'config', {
data: {
'config.yaml': [
`tunnel: ${props.tunnelName}`, // Tunnel name must match the credentials
'credentials-file: /etc/cloudflared/creds/credentials.json', // defined by "kplus.Secret" below
`metrics: "[::]:2000"`,
'no-autoupdate: true',
'protocol: http2', // quic is blocked in the LINZ network
].join('\n'),
},
});

// Secret credentials for the tunnel
const secret = new kplus.Secret(this, 'secret');
secret.addStringData(
'credentials.json',
JSON.stringify({
AccountTag: props.accountId,
TunnelID: props.tunnelId,
TunnelSecret: props.tunnelSecret,
}),
);

new kplus.Deployment(this, 'tunnel', {
// Ensure two tunnels are active
replicas: 2,
containers: [
{
name: 'cloudflared',
image: props.accountId + '.dkr.ecr.ap-southeast-2.amazonaws.com/eks:cloudflared-2023.8.2',
args: ['tunnel', '--loglevel', 'trace', '--config', '/etc/cloudflared/config/config.yaml', 'run'],
volumeMounts: [
{ volume: kplus.Volume.fromConfigMap(this, 'mount-config', cm), path: '/etc/cloudflared/config' },
{ volume: kplus.Volume.fromSecret(this, 'mount-secret', secret), path: '/etc/cloudflared/creds' },
],
resources: { memory: { request: Size.mebibytes(128) } },
// Cloudflared runs as root
securityContext: { ensureNonRoot: false },
},
],
securityContext: { ensureNonRoot: false },
});
}
}
35 changes: 35 additions & 0 deletions infra/util/ssm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SSM } from '@aws-sdk/client-ssm';

const ssm = new SSM();

/**
* Attempt to load a collection of SSM parameters throwing if any parameter cannot be found
*
* @example
* ```typescript
* const result = fetchSsmParameters({ clientId: '/eks/client-id' })
* result.clientId // Value of '/eks/client-id'
* ```
*
* @throws if a parameter is missing from the store
*/
export async function fetchSsmParameters<T extends Record<string, string>>(query: T): Promise<T> {
console.log('FetchSSM', Object.values(query));
const ret = await ssm.getParameters({ Names: Object.values(query) });

const output: Record<string, string> = {};
const missing: string[] = [];
for (const [key, parameterName] of Object.entries(query)) {
const val = ret.Parameters?.find((f) => f.Name === parameterName);
if (val == null || val.Value == null) {
missing.push(parameterName);
continue;
}
output[key] = val.Value;
}

if (missing.length > 0) {
throw new Error('Missing SSM Parameters: ' + missing.join(', '));
}
return output as T;
}
Loading

0 comments on commit 4db3623

Please sign in to comment.