Set up Amazon S3 Upload Provider Plugin for Your Strapi App

I just thought I’d add a working example with the config I have made to my code to get it to work seeing as I had a lot of trouble getting things to display correctly on both Strapi and my front-end.
I use cloudformation to deploy my service on the cloud so I have attached a couple of code extract in the hope it helps someone.

I have added the following variables in my .env file. Note for local development I still use the local upload provider.

CORS_ALLOWED_ORIGINS=["*"]

#AWS + s3 upload provider variables
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
UPLOAD_AWS_BUCKET_NAME=
UPLOAD_IMAGE_PROVIDER=local
UPLOAD_MEDIA_SOURCE=["'self'", "data:", "blob:", "dl.airtable.com"]

This is my middlewares.ts config file:

export default ({ env }) => [
  "strapi::errors",
  {
    name: "strapi::security",
    config: {
      contentSecurityPolicy: {
        useDefaults: true,
        directives: {
          "connect-src": ["'self'", "https:"],
          "img-src": env.array("UPLOAD_MEDIA_SOURCE", ["*"]),
          "media-src": env.array("UPLOAD_MEDIA_SOURCE", ["*"]),
          upgradeInsecureRequests: null,
        },
      },
    },
  },
  {
    name: "strapi::cors",
    config: {
      origin: env.array("CORS_ALLOWED_ORIGINS", ["*"]),
    },
  },
  "strapi::poweredBy",
  "strapi::logger",
  "strapi::query",
  "strapi::body",
  "strapi::session",
  "strapi::favicon",
  "strapi::public",
];

Finally, this is my plugins.ts files

export default ({ env }) => ({
  "users-permissions": {
    config: {
      jwt: {
        expiresIn: "7d",
      },
    },
  },
  upload: {
    config: {
      provider: env("UPLOAD_IMAGE_PROVIDER", "local"),
      providerOptions: {
        accessKeyId: env("AWS_ACCESS_KEY_ID"),
        secretAccessKey: env("AWS_SECRET_ACCESS_KEY"),
        region: env("AWS_REGION"),
        params: {
          Bucket: env("UPLOAD_AWS_BUCKET_NAME"),
        },
      },
    },
  },
});

These are the values I pass into my task definition - this is a simple AWS::ECS::TaskDefinition to host strapi using Fargate:

          Environment:
            - Name: NODE_ENV
              Value: !Ref Environment
            - Name: CORS_ALLOWED_ORIGINS
              Value: !Sub "[https://www.${DomainAddress}, https://${ApiDomainAddress}]"
            - Name: DATABASE_NAME
              Value: !Sub "strapi_${Environment}"
            - Name: DATABASE_PORT
              Value: !Ref DatabasePort
            - Name: DATABASE_HOST
              Value: !GetAtt DBInstance.Endpoint.Address
            - Name: DATABASE_USERNAME
              Value: !Sub "{{resolve:secretsmanager:${RDSInstanceRotationSecret}::username}}"
            - Name: DATABASE_PASSWORD
              Value: !Sub "{{resolve:secretsmanager:${RDSInstanceRotationSecret}::password}}"
            - Name: UPLOAD_AWS_BUCKET_NAME
              Value: !Ref StrapiImageUploadBucket
            - Name: UPLOAD_MEDIA_SOURCE
              Value: !Sub "['self', data:, blob:, dl.airtable.com, ${StrapiImageUploadBucket.RegionalDomainName}]"
            - Name: UPLOAD_IMAGE_PROVIDER
              Value: !Ref UploadImageProvider

The main thing to consider is that the AWS credentials and region are read from the Task’s AWS::IAM::Role so I don’t pass these into the task itself. The bucket declaration itself is a very basic AWS::S3::Bucket with mostly the default configuration. See below:

  StrapiImageUploadBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Sub "${ServiceName}-strapi-image"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"
      LifecycleConfiguration:
        Rules:
          - Id: !Sub "${ServiceName}-strapi-image"
            NoncurrentVersionExpirationInDays: !Ref BucketLifecycleRetention
            Status: Enabled

Happy to provide any help or further examples should anybody require these.

Cheers