다음을 통해 공유


자습서: TypeScript를 사용하여 Azure Storage Blob에 이미지 업로드

이 자습서에서는 자격 증명을 노출하지 않고 브라우저에서 Azure Blob Storage로 직접 파일을 업로드하는 방법을 보여 줍니다. TypeScript를 사용하여 안전한 키 없는 인증을 위해 SAS(공유 액세스 서명) 토큰 및 관리 ID를 사용하여 발레 키 패턴을 구현합니다.

샘플 애플리케이션에는 다음이 포함됩니다.

  • 시간 제한 SAS 토큰을 생성하는 Fastify API
  • Azure Storage에 직접 파일을 업로드하는 React 프런트 엔드
  • Azure Developer CLI를 사용하여 배포를 위한 코드로서의 인프라

이 자습서를 마치면 브라우저에 스토리지 자격 증명을 노출하지 않고 보안 파일 업로드를 보여 주는 작업 애플리케이션이 Azure Container Apps에 배포됩니다.

필수 구성 요소

시작하기 전에 다음이 있는지 확인합니다.

이 자습서에서는 브라우저에서 미리 구성된 개발 환경을 제공하는 GitHub Codespaces를 사용합니다. 로컬 설정이 필요하지 않습니다.

아키텍처

업로드 흐름을 보여 주는 Azure 아키텍처 다이어그램: 사용자가 웹앱 프런트 엔드에서 파일을 선택하고, 프런트 엔드가 API 앱 백 엔드에서 SAS 토큰을 요청하고, 백 엔드는 관리 ID에서 사용자 위임 키를 가져오고, 스토리지 Blob 컨테이너에서 SAS 토큰을 생성하고, 프런트 엔드는 SAS 토큰을 사용하여 스토리지에 파일을 직접 업로드하고, 업로드된 파일을 나열하는 백 엔드 쿼리 스토리지를 보여 줍니다. Container Registry는 두 앱에 대한 컨테이너 이미지를 제공합니다.

프런트 엔드는 API에서 SAS 토큰을 요청한 다음, Azure Storage에 직접 파일을 업로드합니다. 업로드 후 API는 표시할 읽기 전용 SAS 토큰이 있는 업로드된 모든 파일을 나열합니다.

파일 선택 단추와 컨테이너 이름 업로드가 표시된 'Azure Storage에 파일 업로드'라는 웹앱의 스크린샷

주요 개념

사용자 위임 SAS 토큰

애플리케이션은 보안, 키 없는 인증을 위해 사용자 위임 SAS 토큰을 사용합니다. 이러한 토큰은 관리 ID를 통해 Microsoft Entra ID 자격 증명으로 서명됩니다. API는 특정 권한(읽기, 쓰기 또는 삭제)을 사용하여 수명이 짧은 토큰(10-60분)을 생성하여 브라우저가 자격 증명을 노출하지 않고 파일을 스토리지에 직접 업로드할 수 있도록 합니다.

Azure 개발자 CLI 배포

를 사용하여 전체 인프라를 배포합니다 azd up. 이는 React 프런트 엔드 및 Fastify API 백 엔드에 대한 Azure Container Apps를 프로비전하고, 관리 ID를 구성하고, RBAC 권한을 할당합니다. 인프라는 Azure Well-Architected Framework 원칙에 따라 Bicep 템플릿을 사용하며, 적절한 경우 Azure Verified Modules를 사용합니다.

개발 컨테이너 환경

이 자습서의 전체 샘플 코드GitHub Codespaces 또는 로컬 Visual Studio Code의 개발 컨테이너를 사용합니다.

비고

개발자 컨테이너 확장을 사용하여 Visual Studio Code에서 이 자습서를 로컬로 실행할 수도 있습니다. 전체 샘플 코드에는 개발 컨테이너 구성이 포함됩니다.

GitHub Codespaces에서 샘플 열기

GitHub Codespaces는 모든 종속성이 미리 설치된 브라우저 기반 VS Code 환경을 제공합니다.

중요하다

모든 GitHub 계정은 매달 무료 시간에 Codespace를 사용할 수 있습니다. 자세한 내용은 GitHub Codespaces 월별 포함된 스토리지 및 코어 시간참조하세요.

  1. 웹 브라우저에서 샘플 리포지토리를 열고 코드>에서 메인 브랜치에 코드스페이스 생성을 선택합니다.

    파일로 이동, 파일 추가 및 녹색 코드 단추가 강조 표시된 GitHub 리포지토리 페이지의 스크린샷.

  2. 개발 컨테이너가 시작될 때까지 기다립니다. 이 시작 프로세스는 몇 분 정도 걸릴 수 있습니다. 이 자습서의 나머지 단계는 이 개발 컨테이너의 컨텍스트에서 수행합니다.

샘플 배포

  1. Azure에 로그인합니다.

    azd auth login
    
  2. 리소스를 프로비전하고 샘플을 호스팅 환경에 배포합니다.

    azd up
    

    메시지가 표시되면 다음 정보를 입력합니다.

    프롬프트 입력하세요
    고유한 환경 이름 입력 secure-upload
    사용할 Azure 구독 선택 목록에서 구독 선택
    'location' 인프라 매개 변수의 값을 입력합니다. 사용 가능한 위치에서 선택

    또는 프로비전된 리소스를 보고 배포 출력을 보려면 다음 명령을 실행하여 프롬프트 없이 배포할 수 있습니다.

    azd provision
    

    그런 다음, 이 명령을 실행하여 애플리케이션 코드를 배포합니다.

    azd deploy
    

    API 또는 웹앱 코드를 변경하는 경우 다음 명령 중 하나를 사용하여 애플리케이션 코드만 다시 배포할 수 있습니다.

    azd deploy app
    azd deploy api
    
  3. 배포가 완료되면 터미널에 표시되는 배포된 웹앱의 URL을 확인합니다.

      (✓) Done: Deploying service app
      - Endpoint: https://app-gp2pofajnjhy6.calmtree-87e53015.eastus2.azurecontainerapps.io/
    

    예제 URL입니다. URL은 달라집니다.

샘플 사용해 보기

  1. 새 브라우저 탭에서 배포된 웹앱을 열고 업로드할 PNG 파일을 선택합니다. 폴더에서 ./docs/media 여러 PNG 파일을 사용할 수 있습니다.

    파일 선택 단추 및 컨테이너 이름 업로드를 보여 주는 Azure Storage에 파일을 업로드하는 웹앱의 스크린샷

  2. SAS 토큰 가져오기를 선택한 다음 파일 업로드를 선택합니다.

  3. 업로드 단추 아래 갤러리에서 업로드된 파일을 봅니다.

    Azure Storage에 daisies.jpg 업로드한 후 파일 이름, SAS URL, 업로드 상태 및 이미지 썸네일을 보여 주는 웹앱의 스크린샷

방금 무슨 일이 일어났나요?

  • 시간이 제한된 쓰기 전용 SAS 토큰을 사용하여 브라우저에서 Azure Storage로 직접 업로드된 파일
  • 갤러리 이미지는 읽기 전용 SAS 토큰을 사용하여 Azure Storage에서 직접 로드됩니다.
  • 브라우저에 인증 비밀이 노출되지 않았습니다.

코드 작동 방식

이제 애플리케이션이 작동하는 것을 살펴보았으므로 코드에서 보안 파일 업로드를 구현하는 방법을 살펴봅니다. 애플리케이션에는 다음 두 가지 주요 부분이 있습니다.

  1. API 백 엔드 - Azure를 사용하여 인증하고 SAS 토큰을 생성합니다.
  2. React 프런트 엔드 - SAS 토큰을 사용하여 Azure Storage에 직접 파일 업로드

다음 섹션에서는 주요 코드 구현을 안내합니다.

SAS 토큰을 생성하고 파일을 나열하는 API 서버

API 서버는 Azure Storage에 인증하고 브라우저에서 사용할 시간 제한 SAS 토큰을 생성합니다.

관리 ID를 사용한 인증

애플리케이션은 인증을 위해 관리 ID가 있는 사용자 위임 키를 사용하며, 이는 Azure 애플리케이션에 가장 안전한 방법입니다. 다음 순서로 ChainedTokenCredential 인증 방법을 시도합니다.

  1. Azure:ManagedIdentityCredential (Container Apps ID)
  2. 로컬 개발: AzureCliCredential (세션 az login )
// From: packages/api/src/lib/azure-storage.ts
export function getCredential(): ChainedTokenCredential {
  if (!_credential) {
    const clientId = process.env.AZURE_CLIENT_ID;
    
    // Create credential chain with ManagedIdentity first
    const credentials = [
      new ManagedIdentityCredential(clientId ? { clientId } : undefined),
      new AzureCliCredential()
    ];
    
    _credential = new ChainedTokenCredential(...credentials);
  }
  return _credential;
}

인증 후, Azure Storage와 상호 작용하기 위해 BlobServiceClient을 만드십시오.

// From: packages/api/src/lib/azure-storage.ts
export function getBlobServiceClient(accountName: string): BlobServiceClient {
  const credential = getCredential();
  const url = `https://${accountName}.blob.core.windows.net`;
  
  return new BlobServiceClient(url, credential);
}

사용자 위임 키를 사용하여 SAS 토큰 생성

SAS 토큰에는 스토리지 계정 키 대신 Microsoft Entra ID 자격 증명을 사용하여 토큰을 인증하는 사용자 위임 키가 필요합니다. 키는 특정 시간 범위에 유효합니다.

const startsOn = new Date();
const expiresOn = new Date(startsOn.valueOf() + minutes * 60 * 1000);

const userDelegationKey = await blobServiceClient.getUserDelegationKey(
  startsOn,
  expiresOn
);

파일 업로드에 대한 쓰기 전용 SAS 토큰 생성

파일 업로드의 경우 API는 데이터를 읽거나 삭제할 수 없는 쓰기 전용 토큰을 생성합니다. 토큰은 10분 후에 만료됩니다.

// From: packages/api/src/routes/sas.ts
const DEFAULT_SAS_TOKEN_PERMISSION = 'w';
const DEFAULT_SAS_TOKEN_EXPIRATION_MINUTES = 10;

const sasToken = generateBlobSASQueryParameters(
  {
    containerName: container,
    blobName: file,
    permissions: BlobSASPermissions.parse(permission),
    startsOn,
    expiresOn
  },
  userDelegationKey,
  accountName
).toString();

const sasUrl = `${blobClient.url}?${sasToken}`;

사용 가능한 권한 수준:

  • 'r' - 읽기(다운로드/보기)
  • 'w' - 쓰기(업로드/덮어쓰기) - 업로드에 사용됨
  • 'd' -삭제
  • 'c' -만들
  • 'a' - 추가(Blob 추가)

파일을 나열하고 보기 위한 읽기 전용 SAS 토큰 생성

파일을 나열하고 표시하기 위해 API는 60분 후에 만료되는 읽기 전용 토큰을 생성합니다.

// From: packages/api/src/routes/list.ts
const LIST_SAS_TOKEN_PERMISSION = 'r';
const LIST_SAS_TOKEN_EXPIRATION_MINUTES = 60;

const sasToken = generateBlobSASQueryParameters(
  {
    containerName: container,
    blobName: blob.name,
    permissions: BlobSASPermissions.parse(LIST_SAS_TOKEN_PERMISSION),
    startsOn,
    expiresOn
  },
  userDelegationKey,
  accountName
).toString();

const sasUrl = `${blobClient.url}?${sasToken}`;

API 서버에서 웹앱 클라이언트 요청 및 SAS 토큰 수신

React 프런트 엔드는 API에서 SAS 토큰을 요청하고 이를 사용하여 브라우저에서 Azure Storage에 파일을 직접 업로드합니다.

프런트 엔드는 3단계 프로세스를 따릅니다.

  1. 특정 파일에 대한 API에서 SAS 토큰 요청
  2. SAS 토큰 URL을 사용하여 Azure Storage에 직접 업로드
  3. 읽기 전용 SAS 토큰을 사용하여 업로드된 파일 목록 가져오기 및 표시

이 아키텍처는 백 엔드를 경량으로 유지합니다. 토큰만 생성하고 파일 데이터를 처리하지 않습니다.

API 서버에서 Blob Storage SAS 토큰 요청

사용자가 파일을 선택하고 "SAS 토큰 가져오기"를 클릭하면 프런트 엔드는 API에서 쓰기 전용 SAS 토큰을 요청합니다.

// From: packages/app/src/App.tsx
const handleFileSasToken = () => {
  const permission = 'w'; // write-only
  const timerange = 10;   // 10 minutes expiration

  if (!selectedFile) return;

  // Build API request URL
  const url = `${API_URL}/api/sas?file=${encodeURIComponent(
    selectedFile.name
  )}&permission=${permission}&container=${containerName}&timerange=${timerange}`;

  fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  })
    .then((response) => {
      if (!response.ok) {
        throw new Error(`Error: ${response.status} ${response.statusText}`);
      }
      return response.json();
    })
    .then((data: SasResponse) => {
      const { url } = data;
      setSasTokenUrl(url); // Store the SAS URL for upload
    });
};

어떻게 되나요?

  • 프런트 엔드는 다음을 보냅니다. GET /api/sas?file=photo.jpg&permission=w&container=upload&timerange=10
  • API는 다음을 반환합니다. { url: "https://storageaccount.blob.core.windows.net/upload/photo.jpg?sv=2024-05-04&..." }
  • 이 URL은 10분 동안 유효하며 해당 특정 Blob에 대한 쓰기 전용 액세스 권한을 부여합니다.

SAS 토큰을 사용하여 Blob Storage에 직접 업로드

SAS 토큰 URL이 수신되면 프런트 엔드는 파일을 ArrayBuffer로 변환하고 파일을 Azure Storage에 직접 업로드하여 API를 완전히 무시합니다. 이렇게 하면 서버 부하가 줄어들고 성능이 향상됩니다.

파일을 ArrayBuffer로 변환합니다.

// From: packages/app/src/lib/convert-file-to-arraybuffer.ts
export function convertFileToArrayBuffer(file: File): Promise<ArrayBuffer | null> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = () => {
      const arrayBuffer = reader.result;
      resolve(arrayBuffer as ArrayBuffer);
    };

    reader.onerror = () => {
      reject(new Error('Error reading file.'));
    };

    reader.readAsArrayBuffer(file);
  });
}

그런 다음, 원본 BlockBlobClient@azure/storage-blob 사용하여 SAS 토큰 URL을 사용하여 파일 데이터를 업로드합니다.

// From: packages/app/src/App.tsx
const handleFileUpload = () => {
  console.log('SAS Token URL:', sasTokenUrl);

  // Convert file to ArrayBuffer
  convertFileToArrayBuffer(selectedFile as File)
    .then((fileArrayBuffer) => {
      if (fileArrayBuffer === null || fileArrayBuffer.byteLength < 1) {
        throw new Error('Failed to convert file to ArrayBuffer');
      }

      // Create Azure Storage client with SAS URL
      const blockBlobClient = new BlockBlobClient(sasTokenUrl);
      
      // Upload directly to Azure Storage
      return blockBlobClient.uploadData(fileArrayBuffer);
    })
    .then((uploadResponse) => {
      if (!uploadResponse) {
        throw new Error('Upload failed - no response from Azure Storage');
      }
      setUploadStatus('Successfully finished upload');
      
      // After upload, fetch the updated list of files
      const listUrl = `${API_URL}/api/list?container=${containerName}`;
      return fetch(listUrl);
    });
};

핵심 사항:

  • 파일이 API 서버를 통과하지 않습니다.
  • 업로드가 브라우저에서 Azure Storage로 직접 이동합니다.
  • SAS 토큰은 요청을 인증합니다.
  • 파일 처리에 대한 서버 대역폭 또는 처리 비용 없음

Azure Storage에서 직접 파일을 가져오고 썸네일 이미지를 표시합니다.

성공적으로 업로드된 후 프런트 엔드는 컨테이너의 모든 파일 목록을 가져옵니다. 목록의 각 파일에는 고유한 읽기 전용 SAS 토큰이 함께 제공됩니다.

// From: packages/app/src/App.tsx
const listUrl = `${API_URL}/api/list?container=${containerName}`;

fetch(listUrl)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`Error: ${response.status}`);
    }
    return response.json();
  })
  .then((data: ListResponse) => {
    setList(data.list); // Array of SAS URLs with read permission
  });

응답 예제:

{
  "list": [
    "https://storageaccount.blob.core.windows.net/upload/photo1.jpg?sv=2024-05-04&se=2025-12-18T15:30:00Z&sr=b&sp=r&...",
    "https://storageaccount.blob.core.windows.net/upload/photo2.jpg?sv=2024-05-04&se=2025-12-18T15:30:00Z&sr=b&sp=r&..."
  ]
}

프런트 엔드는 이미지 태그에서 SAS URL을 직접 사용합니다. 브라우저는 포함된 읽기 전용 토큰을 사용하여 Azure Storage에서 이미지를 가져옵니다.

// From: packages/app/src/App.tsx
<Grid container spacing={2}>
  {list.map((item) => {
    const urlWithoutQuery = item.split('?')[0];
    const filename = urlWithoutQuery.split('/').pop() || '';
    const isImage = filename.endsWith('.jpg') || 
                    filename.endsWith('.png') || 
                    filename.endsWith('.jpeg');
    
    return (
      <Grid item xs={6} sm={4} md={3} key={item}>
        <Card>
          {isImage ? (
            <CardMedia component="img" image={item} alt={filename} />
          ) : (
            <Typography>{filename}</Typography>
          )}
        </Card>
      </Grid>
    );
  })}
</Grid>

작동 방식:

  • 목록의 각 URL에는 읽기 전용 SAS 토큰(sp=r)이 포함됩니다.
  • 브라우저에서 Azure Storage에 직접 GET 요청
  • 인증 필요 없음 - 토큰이 URL에 있음
  • 토큰은 60분 후에 만료됩니다(API에 구성됨).

리소스 정리

이 자습서를 마치면 지속적인 요금이 발생하지 않도록 모든 Azure 리소스를 제거합니다.

azd down

문제 해결

GitHub 리포지토리에서 이 샘플과 관련된 문제를 보고합니다. 문제에 다음을 포함합니다.

  • 문서의 URL
  • 문제가 있는 문서 내의 단계 또는 컨텍스트
  • 개발 환경

샘플 코드

다음 단계

이제 Azure Storage에 파일을 안전하게 업로드하는 방법을 알아보았으므로 다음 관련 항목을 살펴보세요.