Skip to content

Commit e7aee1a

Browse files
authored
Merge pull request #70 from Virtual-Finland-Development/VFD-289-aurora-rds-postgresql-serverless-v-2-pystytys
Database setup for production-like live environments
2 parents 944413e + 80cbba5 commit e7aee1a

11 files changed

Lines changed: 270 additions & 72 deletions

File tree

VirtualFinland.UsersAPI.Deployment/Common/Environments.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ public enum Environments
55
Dev,
66
Test,
77
Staging,
8-
Production
8+
MvpStaging,
9+
MvpProduction
910
}

VirtualFinland.UsersAPI.Deployment/Common/Models/StackSetup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
namespace VirtualFinland.UsersAPI.Deployment.Common.Models;
44

5-
public record StackSetup
5+
public class StackSetup
66
{
77
public InputMap<string> Tags = default!;
88
public bool IsProductionEnvironment;
99
public string Environment = default!;
1010
public string ProjectName = default!;
1111
public string Organization = default!;
1212
public string Region = default!;
13+
public string CreateResourceName(string name) => $"{ProjectName}-{name}-{Environment}".ToLower();
1314
public string GetInfrastructureStackName() => $"{Organization}/infrastructure/{Environment}";
1415
}

VirtualFinland.UsersAPI.Deployment/Features/DatabaseMigratorLambda.cs

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.Text.Json;
44
using Pulumi;
5-
using Pulumi.Aws.Ec2;
65
using Pulumi.Aws.Iam;
76
using Pulumi.Aws.Lambda;
87
using Pulumi.Aws.Lambda.Inputs;
@@ -15,7 +14,7 @@ class DatabaseMigratorLambda
1514
public DatabaseMigratorLambda(Config config, StackSetup stackSetup, VpcSetup vpcSetup, SecretsManager secretsManager)
1615
{
1716
// Lambda function
18-
var execRole = new Role($"{stackSetup.ProjectName}-DatabaseMigratorLambdaRole-{stackSetup.Environment}", new RoleArgs
17+
var execRole = new Role(stackSetup.CreateResourceName("DatabaseMigratorLambdaRole"), new RoleArgs
1918
{
2019
AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary<string, object?>
2120
{
@@ -41,53 +40,26 @@ public DatabaseMigratorLambda(Config config, StackSetup stackSetup, VpcSetup vpc
4140
Tags = stackSetup.Tags
4241
});
4342

44-
var rolePolicyAttachment = new RolePolicyAttachment($"{stackSetup.ProjectName}-DatabaseMigratorLambdaRoleAttachment-{stackSetup.Environment}", new RolePolicyAttachmentArgs
43+
new RolePolicyAttachment(stackSetup.CreateResourceName("DatabaseMigratorLambdaRoleAttachment"), new RolePolicyAttachmentArgs
4544
{
4645
Role = Output.Format($"{execRole.Name}"),
4746
PolicyArn = ManagedPolicy.AWSLambdaVPCAccessExecutionRole.ToString()
4847
});
4948

50-
var secretsManagerReadPolicy = new Policy($"{stackSetup.ProjectName}-DatabaseMigratorLambdaSecretManagerPolicy-{stackSetup.Environment}", new()
51-
{
52-
Description = "Users-API Migration Runner Secrets Get Policy",
53-
PolicyDocument = Output.Format($@"{{
54-
""Version"": ""2012-10-17"",
55-
""Statement"": [
56-
{{
57-
""Effect"": ""Allow"",
58-
""Action"": [
59-
""secretsmanager:GetSecretValue"",
60-
""secretsmanager:DescribeSecret""
61-
],
62-
""Resource"": [
63-
""{secretsManager.Arn}""
64-
]
65-
}}
66-
]
67-
}}"),
68-
Tags = stackSetup.Tags,
69-
});
70-
71-
new RolePolicyAttachment($"{stackSetup.ProjectName}-DatabaseMigratorLambdaRoleAttachment-SecretManager-{stackSetup.Environment}", new RolePolicyAttachmentArgs
49+
new RolePolicyAttachment(stackSetup.CreateResourceName("DatabaseMigratorLambdaRoleAttachment-SecretManager"), new RolePolicyAttachmentArgs
7250
{
7351
Role = execRole.Name,
74-
PolicyArn = secretsManagerReadPolicy.Arn
75-
});
76-
77-
var defaultSecurityGroup = Pulumi.Aws.Ec2.GetSecurityGroup.Invoke(new GetSecurityGroupInvokeArgs()
78-
{
79-
VpcId = vpcSetup.VpcId,
80-
Name = "default"
52+
PolicyArn = secretsManager.ReadPolicy.Arn
8153
});
8254

8355
var functionVpcArgs = new FunctionVpcConfigArgs()
8456
{
85-
SecurityGroupIds = defaultSecurityGroup.Apply(o => $"{o.Id}"),
57+
SecurityGroupIds = new[] { vpcSetup.SecurityGroupId },
8658
SubnetIds = vpcSetup.PrivateSubnetIds
8759
};
8860

8961
var appArtifactPath = Environment.GetEnvironmentVariable("DB_MIGRATOR_ARTIFACT_PATH") ?? config.Require("dbMigratorArtifactPath");
90-
var lambdaFunction = new Function($"{stackSetup.ProjectName}-DatabaseMigrationRunner-{stackSetup.Environment}", new FunctionArgs
62+
var lambdaFunction = new Function(stackSetup.CreateResourceName("DatabaseMigrationRunner"), new FunctionArgs
9163
{
9264
Role = execRole.Arn,
9365
Runtime = "dotnet6",

VirtualFinland.UsersAPI.Deployment/Features/LambdaFunctionUrl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class LambdaFunctionUrl
1010
public LambdaFunctionUrl(StackSetup stackSetup, UsersApiLambdaFunction lambdaFunction)
1111
{
1212

13-
var functionUrl = new FunctionUrl($"{stackSetup.ProjectName}-FunctionUrl-{stackSetup.Environment}", new FunctionUrlArgs
13+
var functionUrl = new FunctionUrl(stackSetup.CreateResourceName("FunctionUrl"), new FunctionUrlArgs
1414
{
1515
FunctionName = lambdaFunction.LambdaFunctionArn,
1616
AuthorizationType = "NONE"

VirtualFinland.UsersAPI.Deployment/Features/PostgresDatabase.cs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
using System.Collections.Immutable;
2-
using System.Linq;
31
using Pulumi;
2+
using Pulumi.Aws.Kms;
43
using Pulumi.Aws.Rds;
4+
using Pulumi.Aws.Rds.Inputs;
55
using Pulumi.Random;
66
using VirtualFinland.UsersAPI.Deployment.Common.Models;
77
using Instance = Pulumi.Aws.Rds.Instance;
@@ -15,7 +15,96 @@ public class PostgresDatabase
1515
{
1616
public PostgresDatabase(Config config, StackSetup stackSetup, VpcSetup vpcSetup)
1717
{
18-
var dbSubNetGroup = new Pulumi.Aws.Rds.SubnetGroup($"{stackSetup.ProjectName}-dbsubnets-{stackSetup.Environment}", new()
18+
if (stackSetup.IsProductionEnvironment)
19+
{
20+
SetupProductionPostgresDatabase(config, stackSetup, vpcSetup);
21+
}
22+
else
23+
{
24+
SetupDevelopmentPostgresDatabase(config, stackSetup, vpcSetup);
25+
}
26+
27+
if (config.GetBoolean("useRdsProxy") == true)
28+
{
29+
var rdsProxy = new RDSProxy(config, stackSetup, this, vpcSetup);
30+
DatabaseConnectionString = rdsProxy.DatabaseConnectionString; // Override the connection string with one from the proxy
31+
}
32+
}
33+
34+
/// <summary>
35+
/// Setup AWS Aurora RDS Serverless V2 for postgresql
36+
/// </summary>
37+
public void SetupProductionPostgresDatabase(Config config, StackSetup stackSetup, VpcSetup vpcSetup)
38+
{
39+
var dbSubNetGroup = new SubnetGroup(stackSetup.CreateResourceName("database-subnets"), new()
40+
{
41+
SubnetIds = vpcSetup.PrivateSubnetIds,
42+
});
43+
44+
var password = new RandomPassword(stackSetup.CreateResourceName("database-password"), new()
45+
{
46+
Length = 16,
47+
Special = false,
48+
OverrideSpecial = "_%@",
49+
});
50+
51+
// Encryption key (KMS)
52+
var encryptionKey = new Key(stackSetup.CreateResourceName("database-encryption-key"), new()
53+
{
54+
Description = "Encryption key for the database",
55+
Tags = stackSetup.Tags,
56+
DeletionWindowInDays = 90, // On deletion, the key will be retained for 30 days before being deleted permanently
57+
});
58+
59+
// AWS Aurora RDS Serverless V2 for postgresql
60+
var auroraCluster = new Cluster(stackSetup.CreateResourceName("database-cluster"), new ClusterArgs()
61+
{
62+
Engine = "aurora-postgresql",
63+
EngineVersion = "13.6",
64+
EngineMode = "provisioned", // serverless v2
65+
Serverlessv2ScalingConfiguration = new ClusterServerlessv2ScalingConfigurationArgs
66+
{
67+
MaxCapacity = 4,
68+
MinCapacity = 0.5,
69+
},
70+
71+
DatabaseName = config.Require("dbName"),
72+
MasterUsername = config.Require("dbAdmin"),
73+
MasterPassword = password.Result,
74+
75+
SkipFinalSnapshot = false,
76+
DeletionProtection = true,
77+
StorageEncrypted = true,
78+
KmsKeyId = encryptionKey.Arn,
79+
80+
DbSubnetGroupName = dbSubNetGroup.Name,
81+
Tags = stackSetup.Tags,
82+
});
83+
84+
new ClusterInstance(stackSetup.CreateResourceName("database-instance"), new()
85+
{
86+
ClusterIdentifier = auroraCluster.ClusterIdentifier,
87+
InstanceClass = "db.serverless",
88+
Engine = "aurora-postgresql",
89+
EngineVersion = auroraCluster.EngineVersion,
90+
Tags = stackSetup.Tags,
91+
});
92+
93+
var DbName = config.Require("dbName");
94+
var DbUsername = config.Require("dbAdmin");
95+
var DbHostName = auroraCluster.Endpoint;
96+
var DbPassword = password.Result;
97+
98+
DatabaseConnectionString = Output.Format($"Host={DbHostName};Database={DbName};Username={DbUsername};Password={DbPassword}");
99+
DBIdentifier = auroraCluster.ClusterIdentifier;
100+
}
101+
102+
/// <summary>
103+
/// Setup AWS RDS for postgresql
104+
/// </summary>
105+
public void SetupDevelopmentPostgresDatabase(Config config, StackSetup stackSetup, VpcSetup vpcSetup)
106+
{
107+
var dbSubNetGroup = new SubnetGroup($"{stackSetup.ProjectName}-dbsubnets-{stackSetup.Environment}", new()
19108
{
20109
SubnetIds = vpcSetup.PrivateSubnetIds,
21110
});
@@ -27,7 +116,7 @@ public PostgresDatabase(Config config, StackSetup stackSetup, VpcSetup vpcSetup)
27116
OverrideSpecial = "_%@",
28117
});
29118

30-
var rdsPostGreInstance = new Instance($"{stackSetup.ProjectName}-postgres-db-{stackSetup.Environment}", new InstanceArgs()
119+
var rdsPostGreInstance = new Instance(stackSetup.CreateResourceName("postgres-db"), new InstanceArgs()
31120
{
32121
Engine = "postgres",
33122
InstanceClass = "db.t3.micro",
@@ -46,11 +135,11 @@ public PostgresDatabase(Config config, StackSetup stackSetup, VpcSetup vpcSetup)
46135
var DbName = config.Require("dbName");
47136
var DbUsername = config.Require("dbAdmin");
48137
var DbHostName = rdsPostGreInstance.Endpoint;
49-
DBIdentifier = rdsPostGreInstance.Identifier;
50138
var DbPassword = password.Result;
51-
DbConnectionString = Output.Format($"Host={DbHostName};Database={DbName};Username={DbUsername};Password={DbPassword}");
139+
DatabaseConnectionString = Output.Format($"Host={DbHostName};Database={DbName};Username={DbUsername};Password={DbPassword}");
140+
DBIdentifier = rdsPostGreInstance.Identifier;
52141
}
53142

54-
public Output<string> DbConnectionString = default!;
55143
public Output<string> DBIdentifier = default!;
144+
public Output<string> DatabaseConnectionString = default!;
56145
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using Pulumi;
2+
using VirtualFinland.UsersAPI.Deployment.Common.Models;
3+
using Pulumi.Aws.Iam;
4+
using System.Text.Json;
5+
using System.Collections.Generic;
6+
using Pulumi.Aws.Rds;
7+
using Pulumi.Aws.Rds.Inputs;
8+
using Pulumi.Random;
9+
10+
namespace VirtualFinland.UsersAPI.Deployment.Features;
11+
12+
public class RDSProxy
13+
{
14+
public RDSProxy(Config config, StackSetup stackSetup, PostgresDatabase database, VpcSetup vpcSetup)
15+
{
16+
// RDS proxy access secret
17+
var username = new RandomPassword(stackSetup.CreateResourceName("rdsproxy-username"), new()
18+
{
19+
Length = 16,
20+
Special = false,
21+
OverrideSpecial = "_%@",
22+
});
23+
var password = new RandomPassword(stackSetup.CreateResourceName("rdsproxy-password"), new()
24+
{
25+
Length = 16,
26+
Special = false,
27+
OverrideSpecial = "_%@",
28+
});
29+
var rdsProxySecretString = Output.Format($"{{\"username\":\"{username.Result}\",\"password\":\"{password.Result}\"}}");
30+
var rdsProxySecret = new SecretsManager(stackSetup, "rdsProxySecret", rdsProxySecretString);
31+
32+
// Create role for rds proxy
33+
var rdsProxyRole = new Role(stackSetup.CreateResourceName("database-proxy-role"), new RoleArgs()
34+
{
35+
AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary<string, object?>
36+
{
37+
{ "Version", "2012-10-17" },
38+
{
39+
"Statement", new[]
40+
{
41+
new Dictionary<string, object?>
42+
{
43+
{ "Action", "sts:AssumeRole" },
44+
{ "Effect", "Allow" },
45+
{ "Sid", "" },
46+
{
47+
"Principal", new Dictionary<string, object?>
48+
{
49+
{ "Service", "rds.amazonaws.com" }
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}),
56+
Tags = stackSetup.Tags
57+
});
58+
59+
new RolePolicyAttachment(stackSetup.CreateResourceName("RdsProxy-SecretManager"), new RolePolicyAttachmentArgs
60+
{
61+
Role = rdsProxyRole.Name,
62+
PolicyArn = rdsProxySecret.Arn
63+
});
64+
65+
// AWS RDS Proxy
66+
var rdsProxy = new Proxy(stackSetup.CreateResourceName("database-proxy"), new()
67+
{
68+
DebugLogging = false,
69+
EngineFamily = "POSTGRESQL",
70+
RequireTls = true,
71+
RoleArn = rdsProxyRole.Arn,
72+
VpcSubnetIds = vpcSetup.PrivateSubnetIds,
73+
VpcSecurityGroupIds = new[] { vpcSetup.SecurityGroupId },
74+
Auths = new[] {
75+
new ProxyAuthArgs
76+
{
77+
AuthScheme = "SECRETS",
78+
Description = "Secrets authentication",
79+
SecretArn = rdsProxySecret.Arn,
80+
IamAuth = "DISABLED"
81+
}
82+
},
83+
Tags = stackSetup.Tags,
84+
});
85+
86+
// RDS Proxy Target
87+
new ProxyTarget(stackSetup.CreateResourceName("database-proxy-target"), new ProxyTargetArgs()
88+
{
89+
DbProxyName = rdsProxy.Name,
90+
DbInstanceIdentifier = database.DBIdentifier,
91+
});
92+
93+
// Set outputs
94+
ProxyEndpoint = rdsProxy.Endpoint;
95+
ProxyIdentifier = rdsProxy.Id;
96+
97+
var DbName = config.Require("dbName");
98+
DatabaseConnectionString = Output.Format($"Host={rdsProxy.Endpoint};Database={DbName};Username={username.Result};Password={password.Result}");
99+
}
100+
101+
[Output]
102+
public Output<string> ProxyEndpoint { get; set; }
103+
public Output<string> ProxyIdentifier { get; set; }
104+
public Output<string> DatabaseConnectionString { get; set; }
105+
}

0 commit comments

Comments
 (0)