> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open-metadata.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Run the S3 Storage Connector Externally

> Use YAML to extract metadata from S3 including files, partitions, and access control details.

export const CodePanel = ({children, fileName = 'config.yaml', showLineNumbers = false}) => {
  const codePanelRef = useRef(null);
  const codeContentRef = useRef(null);
  const isProgrammaticScroll = useRef(false);
  const hoverTimeout = useRef(null);
  useEffect(() => {
    let tries = 0;
    const wrapLines = () => {
      const root = codeContentRef.current;
      if (!root) return;
      const pres = Array.from(root.querySelectorAll('pre'));
      if (!pres.length) {
        if (tries++ < 20) requestAnimationFrame(wrapLines);
        return;
      }
      let globalLine = 1;
      pres.forEach(pre => {
        const code = pre.querySelector('code') || pre;
        if (!code || code.dataset.wrapped === 'true') return;
        const raw = code.textContent || '';
        let lines = raw.split('\n');
        while (lines[0] === '') lines.shift();
        while (lines[lines.length - 1] === '') lines.pop();
        code.innerHTML = lines.map(line => {
          const ln = globalLine++;
          const num = showLineNumbers ? `<span class="line-number">${ln}</span>` : '';
          const safe = line.replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
          return `<span class="code-line" data-line="${ln}">${num}${safe}</span>`;
        }).join('');
        code.dataset.wrapped = 'true';
      });
    };
    wrapLines();
  }, [children, showLineNumbers]);
  useEffect(() => {
    const panel = codePanelRef.current;
    const content = codeContentRef.current;
    if (!panel || !content) return;
    const waitForLines = () => {
      const codeLines = content.querySelectorAll('.code-line');
      if (!codeLines.length) {
        requestAnimationFrame(waitForLines);
        return;
      }
      setupHighlighting(codeLines);
    };
    const setupHighlighting = codeLines => {
      const layout = panel.closest('.split-layout');
      const sections = layout.querySelectorAll('.content-section');
      const parseLines = str => {
        if (!str) return [];
        const out = [];
        str.split(',').forEach(p => {
          if (p.includes('-')) {
            const [s, e] = p.split('-').map(Number);
            for (let i = s; i <= e; i++) out.push(i);
          } else {
            const n = Number(p);
            if (!isNaN(n)) out.push(n);
          }
        });
        return out;
      };
      const clearHighlight = () => {
        codeLines.forEach(l => l.classList.remove('highlighted'));
      };
      const highlight = lines => {
        clearHighlight();
        lines.forEach(n => {
          const el = content.querySelector(`.code-line[data-line="${n}"]`);
          if (el) el.classList.add('highlighted');
        });
      };
      const scrollToLines = lines => {
        if (!lines.length) return;
        const first = lines[0];
        const targetLine = lines.length > 1 ? first : lines[0];
        const el = content.querySelector(`.code-line[data-line="${targetLine}"]`);
        if (!el) return;
        isProgrammaticScroll.current = true;
        const containerRect = content.getBoundingClientRect();
        const elRect = el.getBoundingClientRect();
        const offset = elRect.top - containerRect.top + content.scrollTop;
        const TOP_PADDING = 16;
        content.scrollTo({
          top: Math.max(offset - TOP_PADDING, 0),
          behavior: 'smooth'
        });
        setTimeout(() => {
          isProgrammaticScroll.current = false;
        }, 200);
      };
      const activate = (section, scroll) => {
        if (section.classList.contains('active')) return;
        sections.forEach(s => s.classList.remove('active'));
        section.classList.add('active');
        const lines = parseLines(section.dataset.lines);
        highlight(lines);
        if (scroll) scrollToLines(lines);
      };
      const observer = new IntersectionObserver(entries => {
        if (isProgrammaticScroll.current) return;
        entries.forEach(e => {
          if (e.isIntersecting) activate(e.target, false);
        });
      }, {
        threshold: 0.3,
        rootMargin: '-80px 0px -40% 0px'
      });
      sections.forEach(section => {
        observer.observe(section);
        section.addEventListener('click', () => activate(section, true));
        section.addEventListener('mouseenter', () => {
          clearTimeout(hoverTimeout.current);
          hoverTimeout.current = setTimeout(() => activate(section, true), 80);
        });
      });
      if (sections[0]) activate(sections[0], false);
    };
    waitForLines();
  }, []);
  const handleCopy = e => {
    const btn = e.currentTarget;
    const codeLines = codeContentRef.current?.querySelectorAll('.code-line');
    if (!codeLines || codeLines.length === 0) return;
    const text = Array.from(codeLines).map(line => {
      const clone = line.cloneNode(true);
      const lineNumber = clone.querySelector('.line-number');
      if (lineNumber) lineNumber.remove();
      return clone.textContent;
    }).join('\n');
    if (!text) return;
    navigator.clipboard.writeText(text).then(() => {
      btn.dataset.copied = 'true';
      setTimeout(() => btn.dataset.copied = 'false', 1500);
    });
  };
  return <div className="code-panel" ref={codePanelRef}>
      <div className="code-header">
        {fileName}
        <button className="copy-btn" aria-label="Copy full code" data-copied="false" onClick={handleCopy}>
          <svg className="icon-copy" viewBox="0 0 15 16" fill="currentColor">
            <path d="M10.113 3.124H2.205C1.463 3.124.86 3.655.86 4.31v10.005c0 .654.603 1.186 1.345 1.186h7.908c.742 0 1.345-.532 1.345-1.186V4.31c0-.655-.606-1.186-1.345-1.186Z" />
            <path d="M13.138.5H5.229c-.742 0-1.344.531-1.344 1.186 0 .23.209.414.47.414s.47-.184.47-.414c0-.197.182-.357.404-.357h7.909c.223 0 .404.16.404.357V11.69c0 .196-.181.356-.404.356-.262 0-.47.184-.47.415 0 .23.208.415.47.415.742 0 1.344-.532 1.344-1.186V1.686C14.482 1.03 13.88.5 13.138.5Z" />
          </svg>

          <svg className="icon-check" viewBox="0 0 20 20" fill="currentColor">
            <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-7.25 7.25a1 1 0 01-1.414 0l-3.25-3.25a1 1 0 011.414-1.414l2.543 2.543 6.543-6.543a1 1 0 011.414 0z" clipRule="evenodd" />
          </svg>
        </button>
      </div>

      <div className="code-content" ref={codeContentRef}>
        {children}
      </div>
    </div>;
};

export const ContentSection = ({id, title, lines, children}) => <div className="content-section" data-content-id={id} data-lines={lines}>
    {title && <h4>{title}</h4>}
    {children}
  </div>;

export const ContentPanel = ({children}) => <div className="content-panel">{children}</div>;

export const CodePreview = ({children}) => {
  const [instanceId] = useState(() => `preview-${Math.random().toString(36).slice(2)}`);
  useEffect(() => {
    const nav = document.querySelector('nav') || document.querySelector('header') || document.querySelector('[class*="nav"]');
    if (nav) {
      document.documentElement.style.setProperty('--navbar-height', `${nav.offsetHeight}px`);
    }
  }, []);
  return <div className="split-layout" data-preview-id={instanceId}>
      {children}
    </div>;
};

export const ConnectorDetailsHeader = ({name, icon, stage, availableFeatures, unavailableFeatures = [], availableFeaturesCollate = []}) => {
  const showSubHeading = availableFeatures?.length > 0 || unavailableFeatures?.length > 0 || availableFeaturesCollate?.length > 0;
  const totalAvailableFeatures = [...availableFeatures || [], ...availableFeaturesCollate || []];
  return <div className="container">
      <div className="Heading">
        <div className="flex items-center gap-3">
          {icon && <div className="IconContainer">
              <img src={icon} alt={name} noZoom className="ConnectorIcon" />
            </div>}
          <h1 className="ConnectorName">{name}</h1>
          <span className={`StageBadge ${stage === 'PROD' ? 'prod' : 'beta'}`}>
            {stage}
          </span>
        </div>
      </div>
      {showSubHeading && <div className="SubHeading">
          <div className="FeaturesHeading">Feature List</div>
          <div className="FeaturesList">
            {totalAvailableFeatures.map(feature => <div className="FeatureTag AvailableFeature" key={feature}>
                ✓ {feature}
              </div>)}
            {unavailableFeatures.map(feature => <div className="FeatureTag UnavailableFeature" key={feature}>
                ✕ {feature}
              </div>)}
          </div>
        </div>}
    </div>;
};

<ConnectorDetailsHeader icon="/public/images/connectors/amazon-s3.webp" name="S3 Storage" stage="PROD" availableFeatures={["Metadata"]} unavailableFeatures={[]} />

This page contains the setup guide and reference information for the S3 connector.
Configure and schedule S3 metadata workflows from the CLI:

* [Requirements](#requirements)
* [Metadata Ingestion](#metadata-ingestion)

## How to Run the Connector Externally

To run the Ingestion via the UI you'll need to use the OpenMetadata Ingestion Container, which comes shipped with
custom Airflow plugins to handle the workflow deployment.

If, instead, you want to manage your workflows externally on your preferred orchestrator, you can check
the following docs to run the Ingestion Framework **anywhere**.

<Columns cols={2}>
  <Card title="External Schedulers" href="/v1.12.x/deployment/ingestion">
    Get more information about running the Ingestion Framework Externally
  </Card>
</Columns>

## Requirements

<Card title="OpenMetadata 1.0 or later" href="/v1.12.x/deployment">
  To deploy OpenMetadata, check the Deployment guides.
</Card>

To run the metadata ingestion, we need the following permissions in AWS:

### S3 Permissions

For all the buckets that we want to ingest, we need to provide the following:

* `s3:ListBucket`
* `s3:GetObject`
* `s3:GetBucketLocation`
* `s3:ListAllMyBuckets`
  Note that the `Resources` should be all the buckets that you'd like to scan. A possible policy could be:

```json theme={null}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:ListBucket",
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": [
                "arn:aws:s3:::*"
            ]
        }
    ]
}
```

### CloudWatch Permissions

Which is used to fetch the total size in bytes for a bucket and the total number of files. It requires:

* `cloudwatch:GetMetricData`
* `cloudwatch:ListMetrics`
  The policy would look like:

```json theme={null}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cloudwatch:GetMetricData",
                "cloudwatch:ListMetrics"
            ],
            "Resource": "*"
        }
    ]
}
```

### Python Requirements

<Tip>
  We have support for Python versions **3.9-3.11**
</Tip>

To run the Athena ingestion, you will need to install:

```bash theme={null}
pip3 install "openmetadata-ingestion[athena]"
```

### OpenMetadata Manifest

In any other connector, extracting metadata happens automatically. In this case, we will be able to extract high-level
metadata from buckets, but in order to understand their internal structure we need users to provide an `openmetadata.json`
file at the bucket root.
`Supported File Formats: [ "csv",  "tsv", "avro", "parquet", "json", "json.gz", "json.zip" ]`
You can learn more about this [here](/v1.12.x/connectors/storage). Keep reading for an example on the shape of the manifest file.

## OpenMetadata Manifest

Our manifest file is defined as a [JSON Schema](https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-spec/src/main/resources/json/schema/metadataIngestion/storage/containerMetadataConfig.json),
and can look like this:

<CodePreview>
  <ContentPanel>
    <ContentSection id={1} title="Entries" lines="1-3">
      **Entries**: We need to add a list of `entries`. Each inner JSON structure will be ingested as a child container of the top-level one. In this case, we will be ingesting 7 children.
    </ContentSection>

    <ContentSection id={2} title="Simple Container" lines="4-9">
      **Simple Container**: The simplest container we can have would be structured, but without partitions. Note that we still need to bring information about:

      * **dataPath**: Where we can find the data. This should be a path relative to the top-level container.
      * **structureFormat**: What is the format of the data we are going to find. This information will be used to read the data.
      * **separator**: Optionally, for delimiter-separated formats such as CSV, you can specify the separator to use when reading the file. If you don't, we will use `,` for CSV and `/t` for TSV files.

      After ingesting this container, we will bring in the schema of the data in the `dataPath`.
    </ContentSection>

    <ContentSection id={3} title="Partitioned Container" lines="10-14">
      **Partitioned Container**: We can ingest partitioned data without bringing in any further details.

      By informing the `isPartitioned` field as `true`, we'll flag the container as `Partitioned`. We will be reading the source files schemas', but won't add any other information.
    </ContentSection>

    <ContentSection id={4} title="Single-Partition Container" lines="15-23">
      **Single-Partition Container**: We can bring partition information by specifying the `partitionColumns`. Their definition is based on the [JSON Schema](https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-spec/src/main/resources/json/schema/entity/data/table.json#L232) definition for table columns. The minimum required information is the `name` and `dataType`.

      When passing `partitionColumns`, these values will be added to the schema, on top of the inferred information from the files.
    </ContentSection>

    <ContentSection id={5} title="Multiple-Partition Container" lines="24-33">
      **Multiple-Partition Container**: We can add multiple columns as partitions.

      Note how in the example we even bring our custom `displayName` for the column `dataTypeDisplay` for its type.

      Again, this information will be added on top of the inferred schema from the data files.
    </ContentSection>

    <ContentSection id={6} title="Automated Container Ingestion" lines="34-37">
      **Automated Container Ingestion**: Registering all the data paths one by one can be a time consuming job, to make the automated structure container ingestion you can provide the depth at which all the data is available.

      For example, suppose following is the file hierarchy within your bucket:

      ```
      # prefix/depth1/depth2/depth3
      athena_service/my_database_a/my_schema_a/table_a/date=01-01-2025/data.parquet
      athena_service/my_database_a/my_schema_a/table_a/date=02-01-2025/data.parquet
      athena_service/my_database_a/my_schema_a/table_b/date=01-01-2025/data.parquet
      athena_service/my_database_a/my_schema_a/table_b/date=02-01-2025/data.parquet

      athena_service/my_database_b/my_schema_a/table_a/date=01-01-2025/data.parquet
      athena_service/my_database_b/my_schema_a/table_a/date=02-01-2025/data.parquet
      athena_service/my_database_b/my_schema_a/table_b/date=01-01-2025/data.parquet
      athena_service/my_database_b/my_schema_a/table_b/date=02-01-2025/data.parquet
      ```

      All table folders containing actual data are at depth 3. When you specify `depth: 3` in the manifest entry, all following paths will get registered as containers in OpenMetadata with this single entry:

      ```
      athena_service/my_database_a/my_schema_a/table_a
      athena_service/my_database_a/my_schema_a/table_b
      athena_service/my_database_b/my_schema_a/table_a
      athena_service/my_database_b/my_schema_a/table_b
      ```

      This saves effort - 1 entry instead of 4 individual entries.
    </ContentSection>

    <ContentSection id={7} title="Unstructured Container" lines="38-45">
      **Unstructured Container**: OpenMetadata supports ingesting unstructured files like images, PDFs, etc. We support fetching the file names, size, and tags associated with such files.

      * To ingest a **single unstructured file**: specify the full path of the file in `dataPath`
      * To ingest **specific file types** (e.g., `pdf` & `png`): provide the folder name in `dataPath` and list of extensions in `unstructuredFormats`
      * To ingest **all unstructured files** regardless of type: provide the folder name in `dataPath` and `["*"]` in `unstructuredFormats`
    </ContentSection>
  </ContentPanel>

  <CodePanel fileName="openmetadata.json">
    ```json theme={null}
    {
      "entries": [
        {
          "dataPath": "transactions/",
          "structureFormat": "csv",
          "separator": ","
        },
        {
          "dataPath": "orders/",
          "structureFormat": "parquet",
          "isPartitioned": true
        },
        {
          "dataPath": "users/",
          "structureFormat": "parquet",
          "isPartitioned": true,
          "partitionColumns": [
            {
              "name": "signup_date",
              "dataType": "DATE"
            }
          ]
        },
        {
          "dataPath": "events/",
          "structureFormat": "parquet",
          "isPartitioned": true,
          "partitionColumns": [
            {
              "name": "event_date",
              "dataType": "DATE"
            },
            {
              "name": "region",
              "dataType": "STRING"
            }
          ]
        },
        {
          "depth": 3,
          "structureFormat": "parquet"
        },
        {
          "dataPath": "reports/report.pdf"
        },
        {
          "dataPath": "documents/",
          "unstructuredFormats": ["pdf", "png", "jpg"]
        }
      ]
    }
    ```
  </CodePanel>
</CodePreview>

### Global Manifest

You can also manage a **single** manifest file to centralize the ingestion process for any container, named `openmetadata_storage_manifest.json`.

<CodePreview>
  <ContentPanel>
    <ContentSection id={1} title="Existing Fields" lines="1-15">
      The fields shown above (`dataPath`, `structureFormat`, `isPartitioned`, etc.) are still valid and work the same way in the global manifest.
    </ContentSection>

    <ContentSection id={2} title="Container Name" lines="1-15">
      **Container Name**: Since we are using a single manifest for all your containers, the field `containerName` will help us identify which container (or Bucket in S3, etc.) contains the presented information.

      Each entry in the global manifest must include a `containerName` to specify which bucket or container it belongs to.
    </ContentSection>
  </ContentPanel>

  <CodePanel fileName="openmetadata_storage_manifest.json">
    ```json theme={null}
    {
      "entries": [
        {
          "containerName": "my-s3-bucket-1",
          "dataPath": "transactions/",
          "structureFormat": "csv",
          "separator": ","
        },
        {
          "containerName": "my-s3-bucket-1",
          "dataPath": "orders/",
          "structureFormat": "parquet",
          "isPartitioned": true
        },
        {
          "containerName": "my-s3-bucket-2",
          "dataPath": "users/",
          "structureFormat": "parquet",
          "isPartitioned": true,
          "partitionColumns": [
            {
              "name": "signup_date",
              "dataType": "DATE"
            }
          ]
        },
        {
          "containerName": "my-s3-bucket-2",
          "dataPath": "events/",
          "structureFormat": "json"
        }
      ]
    }
    ```
  </CodePanel>
</CodePreview>

You can also keep local manifests `openmetadata.json` in each container, but if possible, we will always try to pick up the global manifest during the ingestion.

## Metadata Ingestion

All connectors are defined as JSON Schemas.
[Here](https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/storage/s3Connection.json)
you can find the structure to create a connection to Athena.
In order to create and run a Metadata Ingestion workflow, we will follow
the steps to create a YAML configuration able to connect to the source,
process the Entities if needed, and reach the OpenMetadata server.
The workflow is modeled around the following
[JSON Schema](https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-spec/src/main/resources/json/schema/metadataIngestion/workflow.json)

### 1. Define the YAML Config

<CodePreview>
  <ContentPanel>
    <ContentSection id={1} title="Source Configuration" lines="1-3">
      Configure the source type and service name for your S3 connector.
    </ContentSection>

    <ContentSection id={2} title="AWS Configuration" lines="7-23">
      AWS Access Key Credentials

      **awsAccessKeyId** and **awsSecretAccessKey** are used to authenticate and authorize programmatic requests to AWS services.

      An access key consists of:

      * **Access Key ID** (for example, `AKIAIOSFODNN7EXAMPLE`)
      * **Secret Access Key** (for example, `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`)

      Both values must be provided together when using static credentials.

      For more information, see
      [Managing access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html).

      AWS Session Token

      **awsSessionToken** is required when using **temporary security credentials**, such as those obtained via AWS STS.

      The session token must be provided along with the access key ID and secret access key for the duration of the session.

      AWS Region

      **awsRegion** specifies the AWS Region where the target service is deployed (for example, `us-east-1`).

      This is the **only required parameter** when configuring an AWS connection. Other credentials can be resolved automatically using environment variables, AWS profiles, or IAM roles.

      Learn more in the
      [AWS Regions and Availability Zones documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html).

      Custom Endpoint URL

      **endPointURL** is an optional custom endpoint used to connect to an AWS service.

      You may want to specify this when:

      * Using VPC endpoints
      * Connecting to local or AWS-compatible services
      * Overriding the default regional endpoint

      See
      [AWS service endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html) for details.

      AWS Profile Name

      **profileName** specifies the AWS CLI profile to use for authentication.

      Profiles store credentials and configuration in AWS config files.
      If not specified, the `default` profile is used.

      Learn more about
      [Named profiles for the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html).

      Assume Role ARN

      **assumeRoleArn** is the Amazon Resource Name (ARN) of the IAM role to assume.

      This is commonly used for:

      * Cross-account access
      * Delegated permissions
      * Enhanced security setups

      This field is **required** when using Assume Role authentication.

      See the
      [AssumeRole API reference](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html).

      Assume Role Session Name

      **assumeRoleSessionName** identifies the assumed role session.

      This value helps uniquely identify a session when the same role is assumed multiple times or by different principals.

      If not provided, the default value `OpenMetadataSession` is used.

      Assume Role Source Identity

      **assumeRoleSourceIdentity** is an optional source identity passed when assuming a role.

      This value is recorded in AWS CloudTrail logs and can be used to trace actions performed using the assumed role.

      See
      [Source Identity in AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html).
    </ContentSection>

    <ContentSection id={3} title="Bucket Names" lines="8">
      **Bucket Names (Optional)**: Provide the names of buckets that you would want to ingest, if you want to ingest metadata from all buckets or apply a filter to ingest buckets then leave this field empty.
    </ContentSection>

    <ContentSection id={4} title="Connection Options" lines="11">
      **Connection Options (Optional)**: Enter the details for any additional connection options that can be sent to storage service during the connection. These details must be added as Key-Value pairs.
    </ContentSection>

    <ContentSection id={5} title="Connection Arguments" lines="13">
      **Connection Arguments (Optional)**: Enter the details for any additional connection arguments such as security or protocol configs that can be sent to storage service during the connection. These details must be added as Key-Value pairs.
    </ContentSection>

    <ContentSection id={6} title="Source Config" lines="24-46">
      The `sourceConfig` is defined [here](https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-spec/src/main/resources/json/schema/metadataIngestion/storageServiceMetadataPipeline.json):

      <div>
        **containerFilterPattern**: Note that the filter supports regex as include or exclude. You can find examples [here](/connectors/ingestion/workflows/metadata/filter-patterns/database).
      </div>

      <div>
        **storageMetadataConfigSource**: Path to the `openmetadata_storage_manifest.json` global manifest file. It can be located in S3, a local path or as a URL to the file.
      </div>
    </ContentSection>

    <ContentSection id={7} title="Sink Configuration" lines="47-49">
      To send the metadata to OpenMetadata, it needs to be specified as `type: metadata-rest`.
    </ContentSection>

    <ContentSection id={8} title="Workflow Configuration" lines="50-66">
      <div>
        The main property here is the `openMetadataServerConfig`, where you can define the host and security provider of your OpenMetadata installation.
      </div>

      <div>
        **Logger Level**

        You can specify the `loggerLevel` depending on your needs. If you are trying to troubleshoot an ingestion, running with `DEBUG` will give you far more traces for identifying issues.
      </div>

      <div>
        **JWT Token**

        JWT tokens will allow your clients to authenticate against the OpenMetadata server. To enable JWT Tokens, you will get more details [here](/deployment/security/enable-jwt-tokens).

        You can refer to the JWT Troubleshooting section [link](/deployment/security/jwt-troubleshooting) for any issues in your JWT configuration.
      </div>

      <div>
        **Store Service Connection**

        If set to `true` (default), we will store the sensitive information either encrypted via the Fernet Key in the database or externally, if you have configured any [Secrets Manager](/deployment/secrets-manager).

        If set to `false`, the service will be created, but the service connection information will only be used by the Ingestion Framework at runtime, and won't be sent to the OpenMetadata server.
      </div>

      <div>
        **SSL Configuration**

        If you have added SSL to the [OpenMetadata server](/deployment/security/enable-ssl), then you will need to handle the certificates when running the ingestion too. You can either set `verifySSL` to `ignore`, or have it as `validate`, which will require you to set the `sslConfig.caCertificate` with a local path where your ingestion runs that points to the server certificate file.

        Find more information on how to troubleshoot SSL issues [here](/deployment/security/enable-ssl/ssl-troubleshooting).
      </div>

      <div>
        **ingestionPipelineFQN**

        Fully qualified name of ingestion pipeline, used to identify the current ingestion pipeline.
      </div>
    </ContentSection>
  </ContentPanel>

  <CodePanel fileName="s3_config.yaml">
    ```yaml theme={null}
    source:
      type: s3
      serviceName: local_s3
      serviceConnection:
        config:
          type: S3
          awsConfig:  # REQUIRED - AWS authentication configuration
          bucketNames:
          - s3-testing-1
          - s3-testing-2
          # connectionOptions:
          #   key: value
          # connectionArguments:
          #   key: value
    ```

    ```yaml theme={null}
             awsAccessKeyId: KEY
             awsSecretAccessKey: SECRET
    ```

    ```yaml theme={null}
            # awsSessionToken: TOKEN
    ```

    ```yaml theme={null}
             awsRegion: us-east-2
    ```

    ```yaml theme={null}
            # endPointURL: https://athena.us-east-2.amazonaws.com/custom
    ```

    ```yaml theme={null}
            # profileName: profile
    ```

    ```yaml theme={null}
            # assumeRoleArn: "arn:partition:service:region:account:resource"
    ```

    ```yaml theme={null}
            # assumeRoleSessionName: session
    ```

    ```yaml theme={null}
            # assumeRoleSourceIdentity: identity
    ```

    ```yaml theme={null}
      sourceConfig:
        config:
          type: StorageMetadata
          # containerFilterPattern:
          #   includes:
          #     - container1
          #     - container2
          #   excludes:
          #     - container3
          #     - container4
          # storageMetadataConfigSource:
          ## For S3
          #   securityConfig:
          #     awsAccessKeyId: ...
          #     awsSecretAccessKey: ...
          #     awsRegion: ...
          #   prefixConfig:
          #     containerName: om-glue-test
          #     objectPrefix: <optional prefix>
          ## For HTTP
          #   manifestHttpPath: http://...
          ## For Local
          #   manifestFilePath: /path/to/openmetadata_storage_manifest.json
    ```

    ```yaml theme={null}
    sink:
      type: metadata-rest
      config: {}
    ```

    ```yaml theme={null}
    workflowConfig:
      loggerLevel: INFO  # DEBUG, INFO, WARNING or ERROR
      openMetadataServerConfig:
        hostPort: "http://localhost:8585/api"
        authProvider: openmetadata
        securityConfig:
          jwtToken: "{bot_jwt_token}"
        ## Store the service Connection information
        storeServiceConnection: true  # false
        ## Secrets Manager Configuration
        # secretsManagerProvider: aws, azure or noop
        # secretsManagerLoader: airflow or env
        ## If SSL, fill the following
        # verifySSL: validate  # or ignore
        # sslConfig:
        #   caCertificate: /local/path/to/certificate
    # ingestionPipelineFQN: <service name>.<ingestion name> ## e.g., "my_redshift.metadata"
    ```
  </CodePanel>
</CodePreview>

### 2. Run with the CLI

First, we will need to save the YAML file. Afterward, and with all requirements installed, we can run:

```bash theme={null}
metadata ingest -c <path-to-yaml>
```

Note that from connector to connector, this recipe will always be the same. By updating the YAML configuration,
you will be able to extract metadata from different sources.

## Related

<Columns cols={2}>
  <Card title="Configure Ingestion Externally" href="/v1.12.x/deployment/ingestion">
    Deploy, configure, and manage the ingestion workflows externally.
  </Card>
</Columns>
