Skip to content
Merged
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
32 changes: 32 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Pulumi Deploy Staging
on:
push:
branches:
- main
jobs:
staging:
name: Deploy to Staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: package.json
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-region: ${{ secrets.AWS_REGION }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Install Node.js dependencies for frontend
run: npm ci --legacy-peer-deps
- name: Install Node.js dependencies for Pulumi project
run: npm install
working-directory: deployment/staging
- uses: pulumi/actions@v6
with:
command: up
stack-name: dmnd-tech-org/demand-user-dashboard-staging
work-dir: deployment/staging
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
4 changes: 2 additions & 2 deletions Dockerfile → Dockerfile.staging
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ RUN npm install --omit=dev
RUN mkdir -p /app/data/config

ENV NODE_ENV=production
ENV PORT=8080
ENV PORT=80
ENV CONFIG_DIR=/app/data/config

# tini ensures proper signal handling (Ctrl+C works)
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]

EXPOSE 8080
EXPOSE 80

CMD ["node", "dist/index.js"]
2 changes: 2 additions & 0 deletions deployment/production/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
4 changes: 4 additions & 0 deletions deployment/production/Pulumi.production.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
config:
aws:region: eu-central-1
pulumi:tags:
pulumi:template: aws-typescript
10 changes: 10 additions & 0 deletions deployment/production/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: demand-user-dashboard-frontend
description: A minimal AWS TypeScript Pulumi program
runtime:
name: nodejs
options:
packagemanager: npm
config:
pulumi:tags:
value:
pulumi:template: aws-typescript
61 changes: 61 additions & 0 deletions deployment/production/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# AWS TypeScript Pulumi Template

A minimal Pulumi template for provisioning AWS infrastructure using TypeScript. This template creates an Amazon S3 bucket and exports its name.

## Prerequisites

- Pulumi CLI (>= v3): https://www.pulumi.com/docs/get-started/install/
- Node.js (>= 14): https://nodejs.org/
- AWS credentials configured (e.g., via `aws configure` or environment variables)

## Getting Started

1. Initialize a new Pulumi project:

```bash
pulumi new aws-typescript
```

Follow the prompts to set your:
- Project name
- Project description
- AWS region (defaults to `us-east-1`)

2. Preview and deploy your infrastructure:

```bash
pulumi preview
pulumi up
```

3. When you're finished, tear down your stack:

```bash
pulumi destroy
pulumi stack rm
```

## Project Layout

- `Pulumi.yaml` — Pulumi project and template metadata
- `index.ts` — Main Pulumi program (creates an S3 bucket)
- `package.json` — Node.js dependencies
- `tsconfig.json` — TypeScript compiler options

## Configuration

| Key | Description | Default |
| ------------- | --------------------------------------- | ----------- |
| `aws:region` | The AWS region to deploy resources into | `us-east-1` |

Use `pulumi config set <key> <value>` to customize configuration.

## Next Steps

- Extend `index.ts` to provision additional resources (e.g., VPCs, Lambda functions, DynamoDB tables).
- Explore [Pulumi AWSX](https://www.pulumi.com/docs/reference/pkg/awsx/) for higher-level AWS components.
- Consult the [Pulumi documentation](https://www.pulumi.com/docs/) for more examples and best practices.

## Getting Help

If you encounter any issues or have suggestions, please open an issue in this repository.
173 changes: 173 additions & 0 deletions deployment/production/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import * as aws from '@pulumi/aws';import * as docker from '@pulumi/docker';
import * as pulumi from '@pulumi/pulumi';

const infra = new pulumi.StackReference('dmnd-tech-org/dmnd-cloud/production');
const env = 'production';
const appName = 'udfe';

const vpcId = infra.requireOutput('vpc').apply(vpc => vpc['vpcId']);

const publicSubnetIds = infra
.requireOutput('vpc')
.apply(vpc => vpc['publicSubnetIds']);

const ecsClusterArn = infra
.requireOutput('ecs')
.apply(ecs => (ecs['ecsCluster'] as { arn: string })['arn']);

// ECR repository
const repo = new aws.ecr.Repository(`${env}-${appName}-repo`);

export const userDashboardFrontendRepoUrl = repo.repositoryUrl;

// Build and push Docker image
const image = new docker.Image(`${env}-${appName}-image`, {
imageName: pulumi.interpolate`${repo.repositoryUrl}:latest`,
build: {
context: '../../',
dockerfile: '../../Dockerfile.prod',
platform: 'linux/amd64',
},
registry: repo.registryId.apply(async () => {
const creds = await aws.ecr.getAuthorizationToken({});
const [username, password] = Buffer.from(creds.authorizationToken, 'base64')
.toString()
.split(':');
return {
server: repo.repositoryUrl,
username,
password,
};
}),
});

export const userDashboardFrontendImage = image.imageName;

// CloudWatch log group
const logGroup = new aws.cloudwatch.LogGroup(`${env}-${appName}-log-group`, {
name: `/ecs/${env}-${appName}-log-group`,
retentionInDays: 1,
});

export const userDashboardFrontendLogGroupName = logGroup.name;

// ECS execution role
const taskExecutionRole = new aws.iam.Role(`${env}-${appName}-execution-role`, {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: 'ecs-tasks.amazonaws.com',
}),
});
new aws.iam.RolePolicyAttachment(`${env}-${appName}-execution-role-policy`, {
role: taskExecutionRole.name,
policyArn:
'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
});

// Task definition
const taskDef = new aws.ecs.TaskDefinition(`${env}-${appName}-task`, {
family: `${env}-${appName}`,
cpu: '256',
memory: '512',
networkMode: 'awsvpc',
requiresCompatibilities: ['FARGATE'],
executionRoleArn: taskExecutionRole.arn,
containerDefinitions: pulumi.all([image.repoDigest]).apply(([digest]) =>
JSON.stringify([
{
name: appName,
image: digest,
essential: true,
portMappings: [{containerPort: 80}],
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-group': `/ecs/${env}-${appName}-log-group`,
'awslogs-region': aws.config.region,
'awslogs-stream-prefix': "internal-dashboard-frontend'",
},
},
},
]),
),
});

// ALB security group
const albSg = new aws.ec2.SecurityGroup(`${env}-${appName}-alb-sg`, {
vpcId,
description: 'Allow HTTPS in',
ingress: [
{
protocol: 'tcp',
fromPort: 443,
toPort: 443,
cidrBlocks: ['0.0.0.0/0'],
},
],
egress: [{protocol: '-1', fromPort: 0, toPort: 0, cidrBlocks: ['0.0.0.0/0']}],
});

// ECS security group
const ecsSg = new aws.ec2.SecurityGroup(`${env}-${appName}-ecs-sg`, {
vpcId,
description: 'Allow ALB to reach ECS tasks',
ingress: [
{
protocol: 'tcp',
fromPort: 80,
toPort: 80,
securityGroups: [albSg.id], // 👈 Allow only ALB
},
],
egress: [{protocol: '-1', fromPort: 0, toPort: 0, cidrBlocks: ['0.0.0.0/0']}],
});

// ALB
const alb = new aws.lb.LoadBalancer(`${env}-${appName}-alb`, {
internal: false,
loadBalancerType: 'application',
securityGroups: [albSg.id],
subnets: publicSubnetIds,
});

const targetGroup = new aws.lb.TargetGroup(`${env}-${appName}-tg`, {
port: 80,
protocol: 'HTTP',
targetType: 'ip',
vpcId,
healthCheck: {
path: '/',
protocol: 'HTTP',
},
});

const listener = new aws.lb.Listener(`${env}-${appName}-listener`, {
loadBalancerArn: alb.arn,
port: 80,
protocol: 'HTTP',
defaultActions: [{type: 'forward', targetGroupArn: targetGroup.arn}],
});

export const userDashboardFrontendListener = listener.arn;

// ECS Service
const service = new aws.ecs.Service(`${env}-${appName}-service`, {
cluster: ecsClusterArn,
desiredCount: 1,
launchType: 'FARGATE',
taskDefinition: taskDef.arn,
networkConfiguration: {
subnets: publicSubnetIds,
securityGroups: [ecsSg.id],
assignPublicIp: true,
},
loadBalancers: [
{
targetGroupArn: targetGroup.arn,
containerName: appName,
containerPort: 80,
},
],
});

export const userDashboardFrontendUrl = alb.dnsName;
export const userDashboardFrontendServiceName = service.name;
Loading
Loading