675 lines
21 KiB
Go
675 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"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) {
|
|
resp, err := http.Get("https://checkip.amazonaws.com")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(body)), nil
|
|
}
|
|
|
|
const originId = "encoded-bucket-origin"
|
|
|
|
// AWS managed "CachingOptimized" cache policy (same ID in every account/region).
|
|
const cachingOptimizedPolicyId = "658327ea-f89d-4fab-a63d-7e88639e58f6"
|
|
|
|
// newServiceDatabase creates a login role and a same-named database owned by
|
|
// that role, so the service only ever touches its own database.
|
|
func newServiceDatabase(ctx *pulumi.Context, provider *postgresql.Provider, service string) (*random.RandomPassword, error) {
|
|
password, err := random.NewRandomPassword(ctx, service+"-db-password", &random.RandomPasswordArgs{
|
|
Length: pulumi.Int(24),
|
|
Special: pulumi.Bool(true),
|
|
MinUpper: pulumi.Int(1),
|
|
MinLower: pulumi.Int(1),
|
|
MinNumeric: pulumi.Int(1),
|
|
MinSpecial: pulumi.Int(1),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
role, err := postgresql.NewRole(ctx, service+"-db-role", &postgresql.RoleArgs{
|
|
Name: pulumi.String(service),
|
|
Login: pulumi.Bool(true),
|
|
Password: password.Result,
|
|
}, pulumi.Provider(provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = postgresql.NewDatabase(ctx, service+"-database", &postgresql.DatabaseArgs{
|
|
Name: pulumi.String(service),
|
|
Owner: role.Name,
|
|
}, pulumi.Provider(provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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, *ecs.Service, *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, 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, 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, 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, nil, err
|
|
}
|
|
|
|
_, err = secretsmanager.NewSecretVersion(ctx, name+"-db-secret-version", &secretsmanager.SecretVersionArgs{
|
|
SecretId: secret.ID(),
|
|
SecretString: dbPassword.Result,
|
|
})
|
|
if err != nil {
|
|
return nil, 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, 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, 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, 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, nil, err
|
|
}
|
|
|
|
service, 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, nil, err
|
|
}
|
|
|
|
return repo, service, alb, nil
|
|
}
|
|
|
|
func main() {
|
|
pulumi.Run(func(ctx *pulumi.Context) error {
|
|
// Create an AWS resource (S3 Bucket) that holds encoded assets
|
|
bucket, err := s3.NewBucket(ctx, "encoded-bucket", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Keep the bucket private; CloudFront reaches it via Origin Access Control.
|
|
_, err = s3.NewBucketPublicAccessBlock(ctx, "encoded-bucket-public-access-block", &s3.BucketPublicAccessBlockArgs{
|
|
Bucket: bucket.ID(),
|
|
BlockPublicAcls: pulumi.Bool(true),
|
|
BlockPublicPolicy: pulumi.Bool(true),
|
|
IgnorePublicAcls: pulumi.Bool(true),
|
|
RestrictPublicBuckets: pulumi.Bool(true),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oac, err := cloudfront.NewOriginAccessControl(ctx, "encoded-bucket-oac", &cloudfront.OriginAccessControlArgs{
|
|
Description: pulumi.String("OAC for encoded-bucket assets"),
|
|
OriginAccessControlOriginType: pulumi.String("s3"),
|
|
SigningBehavior: pulumi.String("always"),
|
|
SigningProtocol: pulumi.String("sigv4"),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
distribution, err := cloudfront.NewDistribution(ctx, "encoded-bucket-cdn", &cloudfront.DistributionArgs{
|
|
Enabled: pulumi.Bool(true),
|
|
Comment: pulumi.String("Edge caching for encoded-bucket assets"),
|
|
Origins: cloudfront.DistributionOriginArray{
|
|
&cloudfront.DistributionOriginArgs{
|
|
DomainName: bucket.BucketRegionalDomainName,
|
|
OriginId: pulumi.String(originId),
|
|
OriginAccessControlId: oac.ID(),
|
|
S3OriginConfig: &cloudfront.DistributionOriginS3OriginConfigArgs{
|
|
OriginAccessIdentity: pulumi.String(""),
|
|
},
|
|
},
|
|
},
|
|
DefaultCacheBehavior: &cloudfront.DistributionDefaultCacheBehaviorArgs{
|
|
TargetOriginId: pulumi.String(originId),
|
|
ViewerProtocolPolicy: pulumi.String("redirect-to-https"),
|
|
AllowedMethods: pulumi.ToStringArray([]string{"GET", "HEAD"}),
|
|
CachedMethods: pulumi.ToStringArray([]string{"GET", "HEAD"}),
|
|
Compress: pulumi.Bool(true),
|
|
CachePolicyId: pulumi.String(cachingOptimizedPolicyId),
|
|
},
|
|
Restrictions: &cloudfront.DistributionRestrictionsArgs{
|
|
GeoRestriction: &cloudfront.DistributionRestrictionsGeoRestrictionArgs{
|
|
RestrictionType: pulumi.String("none"),
|
|
},
|
|
},
|
|
ViewerCertificate: &cloudfront.DistributionViewerCertificateArgs{
|
|
CloudfrontDefaultCertificate: pulumi.Bool(true),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Allow only this distribution to read objects from the bucket.
|
|
policy := pulumi.All(bucket.Arn, distribution.Arn).ApplyT(func(args []interface{}) (string, error) {
|
|
bucketArn := args[0].(string)
|
|
distributionArn := args[1].(string)
|
|
|
|
doc := map[string]interface{}{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]interface{}{
|
|
{
|
|
"Sid": "AllowCloudFrontServicePrincipal",
|
|
"Effect": "Allow",
|
|
"Principal": map[string]string{"Service": "cloudfront.amazonaws.com"},
|
|
"Action": "s3:GetObject",
|
|
"Resource": bucketArn + "/*",
|
|
"Condition": map[string]interface{}{
|
|
"StringEquals": map[string]string{"AWS:SourceArn": distributionArn},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, err := json.Marshal(doc)
|
|
return string(b), err
|
|
}).(pulumi.StringOutput)
|
|
|
|
_, err = s3.NewBucketPolicy(ctx, "encoded-bucket-policy", &s3.BucketPolicyArgs{
|
|
Bucket: bucket.ID(),
|
|
Policy: policy,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Small, single-AZ Postgres instance for the assignment: default VPC,
|
|
// no public access, no Multi-AZ, no backups, destroyable without a snapshot.
|
|
vpc, err := ec2.LookupVpc(ctx, &ec2.LookupVpcArgs{Default: pulumi.BoolRef(true)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subnets, err := ec2.GetSubnets(ctx, &ec2.GetSubnetsArgs{
|
|
Filters: []ec2.GetSubnetsFilter{
|
|
{Name: "vpc-id", Values: []string{vpc.Id}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dbSubnetGroup, err := rds.NewSubnetGroup(ctx, "postgres-subnet-group", &rds.SubnetGroupArgs{
|
|
SubnetIds: pulumi.ToStringArray(subnets.Ids),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
myIP, err := currentPublicIP()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
myIPCidr := fmt.Sprintf("%s/32", myIP)
|
|
|
|
dbSecurityGroup, err := ec2.NewSecurityGroup(ctx, "postgres-sg", &ec2.SecurityGroupArgs{
|
|
Description: pulumi.String("Allow Postgres access from within the VPC and the deployers IP"),
|
|
VpcId: pulumi.String(vpc.Id),
|
|
Ingress: ec2.SecurityGroupIngressArray{
|
|
&ec2.SecurityGroupIngressArgs{
|
|
Protocol: pulumi.String("tcp"),
|
|
FromPort: pulumi.Int(5432),
|
|
ToPort: pulumi.Int(5432),
|
|
CidrBlocks: pulumi.ToStringArray([]string{vpc.CidrBlock}),
|
|
},
|
|
&ec2.SecurityGroupIngressArgs{
|
|
Protocol: pulumi.String("tcp"),
|
|
FromPort: pulumi.Int(5432),
|
|
ToPort: pulumi.Int(5432),
|
|
CidrBlocks: pulumi.ToStringArray([]string{myIPCidr}),
|
|
Description: pulumi.String("Deployer IP, for the postgresql provider to create databases"),
|
|
},
|
|
},
|
|
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
|
|
}
|
|
|
|
// Generated once and kept in the stack state, so it stays stable across deploys
|
|
// unless explicitly replaced. RDS master passwords can't contain '/', '@', '"', or spaces.
|
|
dbPassword, err := random.NewRandomPassword(ctx, "postgres-db-password", &random.RandomPasswordArgs{
|
|
Length: pulumi.Int(24),
|
|
Special: pulumi.Bool(true),
|
|
OverrideSpecial: pulumi.String("!#$%&*()-_=+[]{}<>:?"),
|
|
MinUpper: pulumi.Int(1),
|
|
MinLower: pulumi.Int(1),
|
|
MinNumeric: pulumi.Int(1),
|
|
MinSpecial: pulumi.Int(1),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
db, err := rds.NewInstance(ctx, "postgres-db", &rds.InstanceArgs{
|
|
Engine: pulumi.String("postgres"),
|
|
EngineVersion: pulumi.String("16"),
|
|
InstanceClass: pulumi.String("db.t3.micro"),
|
|
AllocatedStorage: pulumi.Int(20),
|
|
StorageType: pulumi.String("gp3"),
|
|
StorageEncrypted: pulumi.Bool(true),
|
|
DbName: pulumi.String("appdb"),
|
|
Username: pulumi.String("postgres"),
|
|
Password: dbPassword.Result,
|
|
DbSubnetGroupName: dbSubnetGroup.Name,
|
|
VpcSecurityGroupIds: pulumi.StringArray{dbSecurityGroup.ID()},
|
|
PubliclyAccessible: pulumi.Bool(true),
|
|
MultiAz: pulumi.Bool(false),
|
|
BackupRetentionPeriod: pulumi.Int(0),
|
|
DeletionProtection: pulumi.Bool(false),
|
|
SkipFinalSnapshot: pulumi.Bool(true),
|
|
ApplyImmediately: pulumi.Bool(true),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pgProvider, err := postgresql.NewProvider(ctx, "postgres-provider", &postgresql.ProviderArgs{
|
|
Host: db.Address,
|
|
Port: db.Port,
|
|
Username: pulumi.String("postgres"),
|
|
Password: dbPassword.Result,
|
|
Sslmode: pulumi.String("require"),
|
|
Superuser: pulumi.Bool(false),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmsPassword, err := newServiceDatabase(ctx, pgProvider, "cms")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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, cmsService, 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, discoveryService, discoveryAlb, err := deployFargateService(ctx, "discovery", 8080,
|
|
cluster, execRole, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup,
|
|
db.Address, db.Port, discoveryPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Gitea Actions CI: one IAM user, scoped to just what the build/deploy
|
|
// pipeline needs — push images to the two ECR repos and force a new
|
|
// deployment on the two ECS services. Credentials are exported as
|
|
// stack secrets for the pipeline to consume (e.g. as Gitea Actions
|
|
// secrets); nothing broader like ecr:*, ecs:*, or task-def changes.
|
|
giteaCIPolicyDocument := pulumi.All(cmsRepo.Arn, discoveryRepo.Arn, cmsService.Arn, discoveryService.Arn).ApplyT(
|
|
func(args []any) (string, error) {
|
|
cmsRepoArn := args[0].(string)
|
|
discoveryRepoArn := args[1].(string)
|
|
cmsServiceArn := args[2].(string)
|
|
discoveryServiceArn := args[3].(string)
|
|
|
|
doc := map[string]any{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]any{
|
|
{
|
|
"Sid": "ECRAuth",
|
|
"Effect": "Allow",
|
|
"Action": "ecr:GetAuthorizationToken",
|
|
"Resource": "*",
|
|
},
|
|
{
|
|
"Sid": "ECRPush",
|
|
"Effect": "Allow",
|
|
"Action": []string{
|
|
"ecr:BatchCheckLayerAvailability",
|
|
"ecr:PutImage",
|
|
"ecr:InitiateLayerUpload",
|
|
"ecr:UploadLayerPart",
|
|
"ecr:CompleteLayerUpload",
|
|
},
|
|
"Resource": []string{cmsRepoArn, discoveryRepoArn},
|
|
},
|
|
{
|
|
"Sid": "ECSDeploy",
|
|
"Effect": "Allow",
|
|
"Action": []string{
|
|
"ecs:UpdateService",
|
|
"ecs:DescribeServices",
|
|
},
|
|
"Resource": []string{cmsServiceArn, discoveryServiceArn},
|
|
},
|
|
},
|
|
}
|
|
b, err := json.Marshal(doc)
|
|
return string(b), err
|
|
},
|
|
).(pulumi.StringOutput)
|
|
|
|
giteaCIUser, err := iam.NewUser(ctx, "gitea-ci-user", &iam.UserArgs{
|
|
Name: pulumi.String("gitea-ci"),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = iam.NewUserPolicy(ctx, "gitea-ci-policy", &iam.UserPolicyArgs{
|
|
User: giteaCIUser.Name,
|
|
Policy: giteaCIPolicyDocument,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Export the name of the bucket
|
|
ctx.Export("bucketName", bucket.ID())
|
|
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)
|
|
ctx.Export("ecsClusterArn", cluster.Arn)
|
|
ctx.Export("cmsServiceArn", cmsService.Arn)
|
|
ctx.Export("discoveryServiceArn", discoveryService.Arn)
|
|
return nil
|
|
})
|
|
}
|