Skip to content

Commit

Permalink
chore: use aws-sdk to increase aws support in eks pods
Browse files Browse the repository at this point in the history
When running in EKS pod with a ServeAccount trusted by an IAM role, the application can get credentials directly from the SA to call AWS API.
  • Loading branch information
amille44420 committed Jan 21, 2022
1 parent 0d8026c commit 646f752
Show file tree
Hide file tree
Showing 13 changed files with 1,331 additions and 248 deletions.
3 changes: 1 addition & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ executors:
APP_DB_URI: mongodb://root:password@127.0.0.1:27017
APP_SESSION_SECRET: localSecret
APP_SMTP_PORT: 1025
APP_STORAGE_ENDPOINT: localhost
APP_STORAGE_PORT: 9000
APP_STORAGE_ENDPOINT: "localhost:9000"
APP_STORAGE_SSL: false
APP_STORAGE_ACCESS_KEY: 'AKIAIOSFODNN7EXAMPLE'
APP_STORAGE_SECRET_KEY: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
Expand Down
3 changes: 1 addition & 2 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ APP_DB_URI="mongodb://root:password@127.0.0.1:27017"
APP_SMTP_PORT=1025

# storage settings
APP_STORAGE_ENDPOINT=localhost
APP_STORAGE_PORT=9000
APP_STORAGE_ENDPOINT="http://localhost:9000"
APP_STORAGE_SSL=false
APP_STORAGE_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"
APP_STORAGE_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Expand Down
3 changes: 1 addition & 2 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ APP_SESSION_SECRET=localSecret
APP_SECURE_COOKIE=false

# storage settings
APP_STORAGE_ENDPOINT=localhost
APP_STORAGE_PORT=9000
APP_STORAGE_ENDPOINT="http://localhost:9000"
APP_STORAGE_SSL=false
APP_STORAGE_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"
APP_STORAGE_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Expand Down
2 changes: 2 additions & 0 deletions devtools/webpack/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const serverConfig = {
isBuildIntentProduction &&
new PackagePlugin({
yarnFile: path.resolve(rootDirname, './yarn.lock'),
// we require aws4 to authenticate with IAM role on the database
additionalModules: ['aws4'],
}),

isBuildIntentProduction && isInteractive && new WebpackBar({ name: 'server', profile: true }),
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
"dependencies": {
"@ant-design/icons": "^4.5.0",
"@apollo/client": "^3.3.21",
"@aws-sdk/client-s3": "^3.46.0",
"@aws-sdk/client-ses": "^3.46.0",
"@aws-sdk/s3-request-presigner": "^3.46.0",
"@babel/runtime": "^7.14.0",
"@graphql-tools/schema": "^8.0.1",
"@promster/express": "^7.0.2",
Expand All @@ -161,6 +164,8 @@
"apollo-server": "^3.0.0",
"apollo-server-express": "^3.0.0",
"apollo-upload-client": "^17.0.0",
"aws-sdk": "^2.1057.0",
"aws4": "^1.11.0",
"bcryptjs": "^2.4.3",
"bson": "^4.4.1",
"bull": "^4.1.1",
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/api/__snapshots__/authenticate.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Array [
Object {
"extensions": Object {
"code": "BAD_USER_INPUT",
"exception": Object {
"stacktrace": Array [
"UserInputError: bad request",
" at reject (/Users/amille/WebstormProjects/starter-kit/src/server/schema/resolvers/mutations/authenticate.ts:30:15)",
" at processTicksAndRejections (node:internal/process/task_queues:96:5)",
],
},
"password": "Invalid credentials",
},
"locations": Array [
Expand All @@ -26,6 +33,13 @@ Array [
Object {
"extensions": Object {
"code": "BAD_USER_INPUT",
"exception": Object {
"stacktrace": Array [
"UserInputError: bad request",
" at reject (/Users/amille/WebstormProjects/starter-kit/src/server/schema/resolvers/mutations/authenticate.ts:30:15)",
" at processTicksAndRejections (node:internal/process/task_queues:96:5)",
],
},
"password": "Invalid credentials",
},
"locations": Array [
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/api/__snapshots__/createAccount.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Array [
Object {
"extensions": Object {
"code": "BAD_USER_INPUT",
"exception": Object {
"stacktrace": Array [
"UserInputError: bad request",
" at Object.mutation (/Users/amille/WebstormProjects/starter-kit/src/server/schema/resolvers/mutations/createAccount.ts:40:19)",
" at processTicksAndRejections (node:internal/process/task_queues:96:5)",
],
},
"password": "Username already taken",
},
"locations": Array [
Expand All @@ -26,6 +33,12 @@ Array [
Object {
"extensions": Object {
"code": "BAD_USER_INPUT",
"exception": Object {
"stacktrace": Array [
"UserInputError: bad request",
" at Object.mutation (/Users/amille/WebstormProjects/starter-kit/src/server/schema/resolvers/mutations/createAccount.ts:26:15)",
],
},
"password": "Password too weak",
},
"locations": Array [
Expand Down
18 changes: 14 additions & 4 deletions src/__tests__/helpers/formData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ import listen from 'test-listen';
import createWebServer from '../../server/core/createWebServer';

export const setupWebService = () => {
let service: Server = null;
let url: string;
let service: Server | null = null;
let url: string | undefined;

return {
get url(): string {
if (!url) {
throw new Error('Server not yet listening');
}

return url;
},
get server(): Server {
if (!service) {
throw new Error('Server not yet ready');
}

return service;
},
initialize: async () => {
Expand All @@ -23,8 +31,10 @@ export const setupWebService = () => {
url = await listen(service);
},
cleanUp: async () => {
// close the server/service
await service.close();
if (service) {
// close the server/service
await service.close();
}
},
};
};
Expand Down
61 changes: 38 additions & 23 deletions src/__tests__/helpers/storage.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,53 @@
import {
HeadBucketCommand,
CreateBucketCommand,
DeleteBucketCommand,
DeleteObjectsCommand,
ListObjectsV2Command,
} from '@aws-sdk/client-s3';
import config from '../../server/core/config';
import { minioClient } from '../../server/core/storage';
import { client } from '../../server/core/storage';

const { bucket } = config.storage;

const emptyBucket = async (): Promise<void> => {
const stream = await minioClient.listObjectsV2(bucket, '', true);

return new Promise((resolve, reject) => {
const objectNames = [];

stream.on('data', object => {
objectNames.push(object.name);
});

stream.on('error', reject);

stream.on('end', async () => {
if (objectNames.length) {
await minioClient.removeObjects(bucket, objectNames);
}

resolve();
});
});
const listObjectsResponse = await client.send(new ListObjectsV2Command({ Bucket: bucket }));

const objectNames = listObjectsResponse.Contents.map(object => object.Key).filter(Boolean);

if (objectNames.length) {
await client.send(
new DeleteObjectsCommand({
Bucket: bucket,
Delete: {
Objects: objectNames.map(objectName => ({ Key: objectName })),
},
})
);
}
};

// eslint-disable-next-line import/prefer-default-export
export const setupEmptyBucket = async (): Promise<void> => {
const alreadyExist = await minioClient.bucketExists(bucket);
const alreadyExist = await (async () => {
try {
// simply look for the head bucket
await client.send(new HeadBucketCommand({ Bucket: bucket }));

return true;
} catch (error) {
console.error(await error);

// for testing purpose we can assume if an error happen the bucket does not exist
return false;
}
})();

if (alreadyExist) {
await emptyBucket();
await minioClient.removeBucket(bucket);
const x = await client.send(new DeleteBucketCommand({ Bucket: bucket }));
console.info(x);
}

await minioClient.makeBucket(bucket, config.storage.provider.region);
await client.send(new CreateBucketCommand({ Bucket: bucket }));
};
85 changes: 61 additions & 24 deletions src/server/core/config.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,67 @@
import chalk from 'chalk';
import type { SMTPSettings } from '../emails';
import { getString, getBoolean, getInteger, getNumber } from './env';

const prefix = 'APP';

const getPrefix = (key: string) => `${prefix}_${key}`;

const getSmtpSettings = (): SMTPSettings => {
const base = {
host: getString(getPrefix('SMTP_HOST'), 'localhost'),
port: getInteger(getPrefix('SMTP_PORT'), 465),
secure: getBoolean(getPrefix('SMTP_SECURE'), false),
};
export type SMTPSettings = {
host: string;
port: number;
secure: boolean;
auth?: { user: string; pass: string };
};

const user = getString(getPrefix('SMTP_USER'));
export type AWSSESSettings = {
endpoint?: string;
region: string;
accessKeyId?: string;
secretAccessKey?: string;
sslEnabled: boolean;
};

if (!user) {
return base;
export type MailerSettings = ({ provider: 'smtp' } & SMTPSettings) | ({ provider: 'aws' } & AWSSESSettings);

const getSmtpSettings = (): MailerSettings => {
const provider = getString(getPrefix('PROVIDER'), 'smtp');

switch (provider) {
case 'smtp': {
const base: MailerSettings = {
provider: 'smtp',
host: getString(getPrefix('SMTP_HOST'), 'localhost'),
port: getInteger(getPrefix('SMTP_PORT'), 465),
secure: getBoolean(getPrefix('SMTP_SECURE'), false),
};

const user = getString(getPrefix('SMTP_USER'));

if (!user) {
return base;
}

return {
...base,
auth: {
user,
pass: getString(getPrefix('SMTP_PASSWORD')),
},
};
}

case 'aws':
return {
provider: 'aws',
endpoint: getString(getPrefix('SMTP_ENDPOINT')),
accessKeyId: getString(getPrefix('SMTP_ACCESS_KEY')),
secretAccessKey: getString(getPrefix('SMTP_SECRET_KEY')),
sslEnabled: getBoolean(getPrefix('SMTP_SSL'), true),
region: getString(getPrefix('SMTP_REGION'), 'ap-southeast-1'),
};

default:
throw new Error('SMTP provider not supported');
}

return {
...base,
auth: {
user,
pass: getString(getPrefix('SMTP_PASSWORD')),
},
};
};

const version = getString('VERSION', '0.0.0-development');
Expand Down Expand Up @@ -73,12 +109,13 @@ const config = {

storage: {
provider: {
endPoint: getString(getPrefix('STORAGE_ENDPOINT')),
accessKey: getString(getPrefix('STORAGE_ACCESS_KEY')),
secretKey: getString(getPrefix('STORAGE_SECRET_KEY')),
useSSL: getBoolean(getPrefix('STORAGE_SSL'), true),
port: getInteger(getPrefix('STORAGE_PORT')),
region: getString(getPrefix('STORAGE_REGION'), 'ap-southeast-1'),
endpoint: getString(getPrefix('STORAGE_ENDPOINT')),
credentials: {
accessKeyId: getString(getPrefix('STORAGE_ACCESS_KEY')),
secretAccessKey: getString(getPrefix('STORAGE_SECRET_KEY')),
},
sslEnabled: getBoolean(getPrefix('STORAGE_SSL'), true),
region: getString(getPrefix('STORAGE_REGION')),
},
bucket: getString(getPrefix('STORAGE_BUCKET'), 'app'),
},
Expand Down
Loading

0 comments on commit 646f752

Please sign in to comment.