FEAT: Deploy ECS For CMS and Discovery
This commit is contained in:
+312
-2
@@ -8,14 +8,23 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudfront"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudwatch"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecr"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lb"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/rds"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
|
||||
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/secretsmanager"
|
||||
"github.com/pulumi/pulumi-postgresql/sdk/v3/go/postgresql"
|
||||
"github.com/pulumi/pulumi-random/sdk/v4/go/random"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
)
|
||||
|
||||
// Matches the region configured in Pulumi.main.yaml (aws:region).
|
||||
const awsRegion = "us-east-1"
|
||||
|
||||
// currentPublicIP returns the caller's public IP, used to scope the RDS
|
||||
// security group to the machine that will actually run `pulumi up`.
|
||||
func currentPublicIP() (string, error) {
|
||||
@@ -71,6 +80,197 @@ func newServiceDatabase(ctx *pulumi.Context, provider *postgresql.Provider, serv
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// deployFargateService gives a service its own ECR repo, log group, task
|
||||
// definition, ECS service, and its own ALB — so it gets its own DNS name
|
||||
// rather than sharing one load balancer with the other service on a
|
||||
// different port. The service's DB password is stored in Secrets Manager and
|
||||
// injected into the container as a secret rather than a plaintext env var.
|
||||
func deployFargateService(
|
||||
ctx *pulumi.Context,
|
||||
name string,
|
||||
containerPort int,
|
||||
cluster *ecs.Cluster,
|
||||
execRole *iam.Role,
|
||||
vpcID string,
|
||||
subnetIDs []string,
|
||||
albSecurityGroup *ec2.SecurityGroup,
|
||||
serviceSecurityGroup *ec2.SecurityGroup,
|
||||
dbAddress pulumi.StringOutput,
|
||||
dbPort pulumi.IntOutput,
|
||||
dbPassword *random.RandomPassword,
|
||||
) (*ecr.Repository, *lb.LoadBalancer, error) {
|
||||
alb, err := lb.NewLoadBalancer(ctx, name+"-alb", &lb.LoadBalancerArgs{
|
||||
LoadBalancerType: pulumi.String("application"),
|
||||
Internal: pulumi.Bool(false),
|
||||
SecurityGroups: pulumi.StringArray{albSecurityGroup.ID()},
|
||||
Subnets: pulumi.ToStringArray(subnetIDs),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repo, err := ecr.NewRepository(ctx, name+"-repo", &ecr.RepositoryArgs{
|
||||
Name: pulumi.String(name),
|
||||
ImageTagMutability: pulumi.String("MUTABLE"),
|
||||
ForceDelete: pulumi.Bool(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
logGroup, err := cloudwatch.NewLogGroup(ctx, name+"-logs", &cloudwatch.LogGroupArgs{
|
||||
Name: pulumi.String("/ecs/" + name),
|
||||
RetentionInDays: pulumi.Int(7),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
secret, err := secretsmanager.NewSecret(ctx, name+"-db-secret", &secretsmanager.SecretArgs{
|
||||
Name: pulumi.String(name + "-db-password"),
|
||||
RecoveryWindowInDays: pulumi.Int(0),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, err = secretsmanager.NewSecretVersion(ctx, name+"-db-secret-version", &secretsmanager.SecretVersionArgs{
|
||||
SecretId: secret.ID(),
|
||||
SecretString: dbPassword.Result,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
secretPolicy := secret.Arn.ApplyT(func(secretArn string) (string, error) {
|
||||
doc := map[string]any{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": []map[string]any{
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "secretsmanager:GetSecretValue",
|
||||
"Resource": secretArn,
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(doc)
|
||||
return string(b), err
|
||||
}).(pulumi.StringOutput)
|
||||
|
||||
_, err = iam.NewRolePolicy(ctx, name+"-secret-access", &iam.RolePolicyArgs{
|
||||
Role: execRole.ID(),
|
||||
Policy: secretPolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
targetGroup, err := lb.NewTargetGroup(ctx, name+"-tg", &lb.TargetGroupArgs{
|
||||
Port: pulumi.Int(containerPort),
|
||||
Protocol: pulumi.String("HTTP"),
|
||||
TargetType: pulumi.String("ip"),
|
||||
VpcId: pulumi.String(vpcID),
|
||||
HealthCheck: &lb.TargetGroupHealthCheckArgs{
|
||||
Path: pulumi.String("/health"),
|
||||
Matcher: pulumi.String("200"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
listener, err := lb.NewListener(ctx, name+"-listener", &lb.ListenerArgs{
|
||||
LoadBalancerArn: alb.Arn,
|
||||
Port: pulumi.Int(80),
|
||||
Protocol: pulumi.String("HTTP"),
|
||||
DefaultActions: lb.ListenerDefaultActionArray{
|
||||
&lb.ListenerDefaultActionArgs{
|
||||
Type: pulumi.String("forward"),
|
||||
TargetGroupArn: targetGroup.Arn,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
containerDefinitions := pulumi.All(repo.RepositoryUrl, logGroup.Name, secret.Arn, dbAddress, dbPort).ApplyT(
|
||||
func(args []any) (string, error) {
|
||||
image := args[0].(string) + ":latest"
|
||||
logGroupName := args[1].(string)
|
||||
secretArn := args[2].(string)
|
||||
dbHost := args[3].(string)
|
||||
dbPort := args[4].(int)
|
||||
|
||||
def := []map[string]any{
|
||||
{
|
||||
"name": name,
|
||||
"image": image,
|
||||
"portMappings": []map[string]any{
|
||||
{"containerPort": containerPort, "protocol": "tcp"},
|
||||
},
|
||||
"environment": []map[string]string{
|
||||
{"name": "DB_HOST", "value": dbHost},
|
||||
{"name": "DB_PORT", "value": fmt.Sprintf("%d", dbPort)},
|
||||
{"name": "DB_NAME", "value": name},
|
||||
{"name": "DB_USER", "value": name},
|
||||
},
|
||||
"secrets": []map[string]string{
|
||||
{"name": "DB_PASSWORD", "valueFrom": secretArn},
|
||||
},
|
||||
"logConfiguration": map[string]any{
|
||||
"logDriver": "awslogs",
|
||||
"options": map[string]string{
|
||||
"awslogs-group": logGroupName,
|
||||
"awslogs-region": awsRegion,
|
||||
"awslogs-stream-prefix": name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(def)
|
||||
return string(b), err
|
||||
},
|
||||
).(pulumi.StringOutput)
|
||||
|
||||
taskDefinition, err := ecs.NewTaskDefinition(ctx, name+"-task", &ecs.TaskDefinitionArgs{
|
||||
Family: pulumi.String(name),
|
||||
Cpu: pulumi.String("256"),
|
||||
Memory: pulumi.String("512"),
|
||||
NetworkMode: pulumi.String("awsvpc"),
|
||||
RequiresCompatibilities: pulumi.StringArray{pulumi.String("FARGATE")},
|
||||
ExecutionRoleArn: execRole.Arn,
|
||||
ContainerDefinitions: containerDefinitions,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, err = ecs.NewService(ctx, name+"-service", &ecs.ServiceArgs{
|
||||
Cluster: cluster.Arn,
|
||||
TaskDefinition: taskDefinition.Arn,
|
||||
DesiredCount: pulumi.Int(1),
|
||||
LaunchType: pulumi.String("FARGATE"),
|
||||
NetworkConfiguration: &ecs.ServiceNetworkConfigurationArgs{
|
||||
Subnets: pulumi.ToStringArray(subnetIDs),
|
||||
SecurityGroups: pulumi.StringArray{serviceSecurityGroup.ID()},
|
||||
AssignPublicIp: pulumi.Bool(true),
|
||||
},
|
||||
LoadBalancers: ecs.ServiceLoadBalancerArray{
|
||||
&ecs.ServiceLoadBalancerArgs{
|
||||
TargetGroupArn: targetGroup.Arn,
|
||||
ContainerName: pulumi.String(name),
|
||||
ContainerPort: pulumi.Int(containerPort),
|
||||
},
|
||||
},
|
||||
}, pulumi.DependsOn([]pulumi.Resource{listener}))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return repo, alb, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
pulumi.Run(func(ctx *pulumi.Context) error {
|
||||
// Create an AWS resource (S3 Bucket) that holds encoded assets
|
||||
@@ -277,12 +477,118 @@ func main() {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = newServiceDatabase(ctx, pgProvider, "cms")
|
||||
cmsPassword, err := newServiceDatabase(ctx, pgProvider, "cms")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = newServiceDatabase(ctx, pgProvider, "discovery")
|
||||
discoveryPassword, err := newServiceDatabase(ctx, pgProvider, "discovery")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ECS Fargate: one cluster, one internet-facing ALB, one service per app.
|
||||
cluster, err := ecs.NewCluster(ctx, "app-cluster", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = ecs.NewClusterCapacityProviders(ctx, "app-cluster-capacity-providers", &ecs.ClusterCapacityProvidersArgs{
|
||||
ClusterName: cluster.Name,
|
||||
CapacityProviders: pulumi.ToStringArray([]string{"FARGATE", "FARGATE_SPOT"}),
|
||||
DefaultCapacityProviderStrategies: ecs.ClusterCapacityProvidersDefaultCapacityProviderStrategyArray{
|
||||
&ecs.ClusterCapacityProvidersDefaultCapacityProviderStrategyArgs{
|
||||
CapacityProvider: pulumi.String("FARGATE"),
|
||||
Weight: pulumi.Int(1),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
albSecurityGroup, err := ec2.NewSecurityGroup(ctx, "alb-sg", &ec2.SecurityGroupArgs{
|
||||
Description: pulumi.String("Allow inbound HTTP to the app load balancers"),
|
||||
VpcId: pulumi.String(vpc.Id),
|
||||
Ingress: ec2.SecurityGroupIngressArray{
|
||||
&ec2.SecurityGroupIngressArgs{
|
||||
Protocol: pulumi.String("tcp"),
|
||||
FromPort: pulumi.Int(80),
|
||||
ToPort: pulumi.Int(80),
|
||||
CidrBlocks: pulumi.ToStringArray([]string{"0.0.0.0/0"}),
|
||||
},
|
||||
},
|
||||
Egress: ec2.SecurityGroupEgressArray{
|
||||
&ec2.SecurityGroupEgressArgs{
|
||||
Protocol: pulumi.String("-1"),
|
||||
FromPort: pulumi.Int(0),
|
||||
ToPort: pulumi.Int(0),
|
||||
CidrBlocks: pulumi.ToStringArray([]string{"0.0.0.0/0"}),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceSecurityGroup, err := ec2.NewSecurityGroup(ctx, "ecs-service-sg", &ec2.SecurityGroupArgs{
|
||||
Description: pulumi.String("Allow the ALB to reach cms and discovery tasks"),
|
||||
VpcId: pulumi.String(vpc.Id),
|
||||
Ingress: ec2.SecurityGroupIngressArray{
|
||||
&ec2.SecurityGroupIngressArgs{
|
||||
Protocol: pulumi.String("tcp"),
|
||||
FromPort: pulumi.Int(0),
|
||||
ToPort: pulumi.Int(65535),
|
||||
SecurityGroups: pulumi.StringArray{albSecurityGroup.ID()},
|
||||
},
|
||||
},
|
||||
Egress: ec2.SecurityGroupEgressArray{
|
||||
&ec2.SecurityGroupEgressArgs{
|
||||
Protocol: pulumi.String("-1"),
|
||||
FromPort: pulumi.Int(0),
|
||||
ToPort: pulumi.Int(0),
|
||||
CidrBlocks: pulumi.ToStringArray([]string{"0.0.0.0/0"}),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execRoleAssumePolicy, err := json.Marshal(map[string]any{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": []map[string]any{
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "sts:AssumeRole",
|
||||
"Principal": map[string]string{"Service": "ecs-tasks.amazonaws.com"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execRole, err := iam.NewRole(ctx, "ecs-task-execution-role", &iam.RoleArgs{
|
||||
AssumeRolePolicy: pulumi.String(execRoleAssumePolicy),
|
||||
ManagedPolicyArns: pulumi.ToStringArray([]string{
|
||||
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmsRepo, cmsAlb, err := deployFargateService(ctx, "cms", 8081,
|
||||
cluster, execRole, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup,
|
||||
db.Address, db.Port, cmsPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
discoveryRepo, discoveryAlb, err := deployFargateService(ctx, "discovery", 8080,
|
||||
cluster, execRole, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup,
|
||||
db.Address, db.Port, discoveryPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -292,6 +598,10 @@ func main() {
|
||||
ctx.Export("cdnDomainName", distribution.DomainName)
|
||||
ctx.Export("dbEndpoint", db.Endpoint)
|
||||
ctx.Export("dbPassword", dbPassword.Result)
|
||||
ctx.Export("cmsUrl", pulumi.Sprintf("http://%s", cmsAlb.DnsName))
|
||||
ctx.Export("discoveryUrl", pulumi.Sprintf("http://%s", discoveryAlb.DnsName))
|
||||
ctx.Export("cmsRepoUrl", cmsRepo.RepositoryUrl)
|
||||
ctx.Export("discoveryRepoUrl", discoveryRepo.RepositoryUrl)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user