FIX: Permissions To s3 And MediaConvert]
Build, Push and Deploy CMS / build-push-deploy (push) Successful in 2m2s

This commit is contained in:
FahdShalhoub
2026-08-16 20:04:04 +03:00
parent 1a44783b1a
commit a07113d061
2 changed files with 196 additions and 15 deletions
+5 -4
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"os"
"thamanyah/cms/v2/internal/handlers"
"thamanyah/cms/v2/internal/services"
@@ -21,7 +22,7 @@ func main() {
concreteS3Client := &services.S3Concrete{
S3Client: s3.NewFromConfig(config),
Bucket: "",
Bucket: os.Getenv("S3_BUCKET"),
}
concreteS3Client.AssertSuccessfulConnection(context.Background())
@@ -30,9 +31,9 @@ func main() {
services.MediaConvertClient = &services.MediaConvertConcrete{
MediaConvertClient: mediaconvert.NewFromConfig(config),
Role: "test",
InputBucket: "",
OutputBucket: "",
Role: os.Getenv("MEDIACONVERT_ROLE_ARN"),
InputBucket: os.Getenv("MEDIACONVERT_INPUT_BUCKET"),
OutputBucket: os.Getenv("MEDIACONVERT_OUTPUT_BUCKET"),
}
mux := http.NewServeMux()
+191 -11
View File
@@ -80,17 +80,29 @@ func newServiceDatabase(ctx *pulumi.Context, provider *postgresql.Provider, serv
return password, nil
}
// envVar is a name/value pair for a container's extra environment variables,
// where the value is only known after other resources are provisioned.
type envVar struct {
Name string
Value pulumi.StringOutput
}
// 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.
// taskRole is optional (nil means no TaskRoleArn is set, i.e. the container
// gets no AWS identity of its own beyond execRole's pull/logs permissions);
// extraEnv is appended to the container's environment on top of the DB_* vars.
func deployFargateService(
ctx *pulumi.Context,
name string,
containerPort int,
cluster *ecs.Cluster,
execRole *iam.Role,
taskRole *iam.Role,
extraEnv []envVar,
vpcID string,
subnetIDs []string,
albSecurityGroup *ec2.SecurityGroup,
@@ -194,7 +206,12 @@ func deployFargateService(
return nil, nil, nil, err
}
containerDefinitions := pulumi.All(repo.RepositoryUrl, logGroup.Name, secret.Arn, dbAddress, dbPort).ApplyT(
baseArgs := []any{repo.RepositoryUrl, logGroup.Name, secret.Arn, dbAddress, dbPort}
for _, ev := range extraEnv {
baseArgs = append(baseArgs, ev.Value)
}
containerDefinitions := pulumi.All(baseArgs...).ApplyT(
func(args []any) (string, error) {
image := args[0].(string) + ":latest"
logGroupName := args[1].(string)
@@ -202,6 +219,16 @@ func deployFargateService(
dbHost := args[3].(string)
dbPort := args[4].(int)
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},
}
for i, ev := range extraEnv {
environment = append(environment, map[string]string{"name": ev.Name, "value": args[5+i].(string)})
}
def := []map[string]any{
{
"name": name,
@@ -209,12 +236,7 @@ func deployFargateService(
"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},
},
"environment": environment,
"secrets": []map[string]string{
{"name": "DB_PASSWORD", "valueFrom": secretArn},
},
@@ -233,7 +255,7 @@ func deployFargateService(
},
).(pulumi.StringOutput)
taskDefinition, err := ecs.NewTaskDefinition(ctx, name+"-task", &ecs.TaskDefinitionArgs{
taskDefinitionArgs := &ecs.TaskDefinitionArgs{
Family: pulumi.String(name),
Cpu: pulumi.String("256"),
Memory: pulumi.String("512"),
@@ -241,7 +263,12 @@ func deployFargateService(
RequiresCompatibilities: pulumi.StringArray{pulumi.String("FARGATE")},
ExecutionRoleArn: execRole.Arn,
ContainerDefinitions: containerDefinitions,
})
}
if taskRole != nil {
taskDefinitionArgs.TaskRoleArn = taskRole.Arn
}
taskDefinition, err := ecs.NewTaskDefinition(ctx, name+"-task", taskDefinitionArgs)
if err != nil {
return nil, nil, nil, err
}
@@ -579,15 +606,168 @@ func main() {
return err
}
// Private bucket for raw video uploads (pre-transcode). Kept separate
// from encoded-bucket, which is fronted by CloudFront/OAC for public
// delivery of finished renditions — raw source video must not be
// reachable through that CDN.
rawUploadsBucket, err := s3.NewBucket(ctx, "raw-uploads-bucket", nil)
if err != nil {
return err
}
_, err = s3.NewBucketPublicAccessBlock(ctx, "raw-uploads-bucket-public-access-block", &s3.BucketPublicAccessBlockArgs{
Bucket: rawUploadsBucket.ID(),
BlockPublicAcls: pulumi.Bool(true),
BlockPublicPolicy: pulumi.Bool(true),
IgnorePublicAcls: pulumi.Bool(true),
RestrictPublicBuckets: pulumi.Bool(true),
})
if err != nil {
return err
}
// Role MediaConvert itself assumes (passed as CreateJobInput.Role by
// the cms service) to read the raw upload and write the encoded
// output — distinct from the ECS task role below.
mediaConvertAssumePolicy, err := json.Marshal(map[string]any{
"Version": "2012-10-17",
"Statement": []map[string]any{
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": map[string]string{"Service": "mediaconvert.amazonaws.com"},
},
},
})
if err != nil {
return err
}
mediaConvertRole, err := iam.NewRole(ctx, "mediaconvert-service-role", &iam.RoleArgs{
AssumeRolePolicy: pulumi.String(mediaConvertAssumePolicy),
})
if err != nil {
return err
}
mediaConvertS3Policy := pulumi.All(rawUploadsBucket.Arn, bucket.Arn).ApplyT(
func(args []any) (string, error) {
rawUploadsArn := args[0].(string)
encodedBucketArn := args[1].(string)
doc := map[string]any{
"Version": "2012-10-17",
"Statement": []map[string]any{
{
"Sid": "ReadRawUploads",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": rawUploadsArn + "/*",
},
{
"Sid": "WriteEncodedOutput",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": encodedBucketArn + "/*",
},
},
}
b, err := json.Marshal(doc)
return string(b), err
},
).(pulumi.StringOutput)
_, err = iam.NewRolePolicy(ctx, "mediaconvert-s3-access", &iam.RolePolicyArgs{
Role: mediaConvertRole.ID(),
Policy: mediaConvertS3Policy,
})
if err != nil {
return err
}
// cms's own ECS task role (distinct from the shared execRole, which
// is only for the ECS agent's pull/logs/secrets access): lets the
// running container call S3 and MediaConvert directly. discovery
// gets none of this since it never touches either API.
cmsTaskRole, err := iam.NewRole(ctx, "cms-task-role", &iam.RoleArgs{
AssumeRolePolicy: pulumi.String(execRoleAssumePolicy),
})
if err != nil {
return err
}
cmsTaskPolicy := pulumi.All(rawUploadsBucket.Arn, mediaConvertRole.Arn).ApplyT(
func(args []any) (string, error) {
rawUploadsArn := args[0].(string)
mediaConvertRoleArn := args[1].(string)
doc := map[string]any{
"Version": "2012-10-17",
"Statement": []map[string]any{
{
"Sid": "ListRawUploadsBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": rawUploadsArn,
},
{
"Sid": "RawUploadsObjectAccess",
"Effect": "Allow",
"Action": []string{
"s3:PutObject",
"s3:GetObject",
},
"Resource": rawUploadsArn + "/*",
},
{
"Sid": "CreateTranscodeJob",
"Effect": "Allow",
"Action": "mediaconvert:CreateJob",
"Resource": "*",
},
{
"Sid": "PassMediaConvertRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": mediaConvertRoleArn,
"Condition": map[string]any{
"StringEquals": map[string]string{
"iam:PassedToService": "mediaconvert.amazonaws.com",
},
},
},
},
}
b, err := json.Marshal(doc)
return string(b), err
},
).(pulumi.StringOutput)
_, err = iam.NewRolePolicy(ctx, "cms-task-role-policy", &iam.RolePolicyArgs{
Role: cmsTaskRole.ID(),
Policy: cmsTaskPolicy,
})
if err != nil {
return err
}
cmsExtraEnv := []envVar{
{Name: "AWS_REGION", Value: pulumi.String(awsRegion).ToStringOutput()},
{Name: "S3_BUCKET", Value: rawUploadsBucket.ID().ToStringOutput()},
{Name: "MEDIACONVERT_INPUT_BUCKET", Value: rawUploadsBucket.ID().ToStringOutput()},
{Name: "MEDIACONVERT_OUTPUT_BUCKET", Value: bucket.ID().ToStringOutput()},
{Name: "MEDIACONVERT_ROLE_ARN", Value: mediaConvertRole.Arn},
}
cmsRepo, cmsService, cmsAlb, err := deployFargateService(ctx, "cms", 8081,
cluster, execRole, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup,
cluster, execRole, cmsTaskRole, cmsExtraEnv, 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,
cluster, execRole, nil, nil, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup,
db.Address, db.Port, discoveryPassword)
if err != nil {
return err