<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daiquiri Team</title>
    <description>The latest articles on DEV Community by Daiquiri Team (@daiquiri_team).</description>
    <link>https://dev.to/daiquiri_team</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F5843%2F166fa27e-054a-4c00-bcc9-e9921a726687.png</url>
      <title>DEV Community: Daiquiri Team</title>
      <link>https://dev.to/daiquiri_team</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daiquiri_team"/>
    <language>en</language>
    <item>
      <title>How to successfully hold a demonstration for an IoT product without any hardware</title>
      <dc:creator>Volodymyr Ryzhkov</dc:creator>
      <pubDate>Mon, 17 Oct 2022 14:49:45 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/how-to-successfully-hold-a-demonstration-for-an-iot-product-without-any-hardware-pbm</link>
      <guid>https://dev.to/daiquiri_team/how-to-successfully-hold-a-demonstration-for-an-iot-product-without-any-hardware-pbm</guid>
      <description>&lt;p&gt;Why is this necessary? Several teams often work on the product, and sometimes one of the teams does not have time to complete its part. It was our case: we were developing the web and back-end parts of the IoT system, and the hardware team on the client's side did not meet the deadline.&lt;/p&gt;

&lt;p&gt;At the same time, the product owner has already scheduled demonstrations with customers, which he would not like to postpone because some meetings can't be scheduled a second time, or the impression will be spoiled. But we found a way to get out of this situation and meet all the deadlines with little effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the product
&lt;/h2&gt;

&lt;p&gt;Our client's company manufactures smart scales. In our collaboration, they were trying to optimize the process of controlling the storage of products in the sales halls of supermarkets, and the idea of their product was as follows:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rbmXbJeS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45nuz2pihlg55c66d0b4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rbmXbJeS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45nuz2pihlg55c66d0b4.png" alt="Image description" width="880" height="2078"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demonstration process
&lt;/h2&gt;

&lt;p&gt;A test shelf had to be installed in one of the supermarkets for a successful demonstration of the system. It was also needed so that our team could test all the subsystems that should work with the shelf.&lt;/p&gt;

&lt;p&gt;According to the demonstration plan, the client had to show the process of adding a new store and its employees to the system and adding shelves to the selected store. Real-time shelf data was displayed on a separate dashboard, and informative reports on shelf performance could also be created and viewed. In case of deviation of temperature or insufficient quantity of goods, employees were notified and were able to solve the problem with the shelf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem solving
&lt;/h2&gt;

&lt;p&gt;What ways out of the situation did we have? We could do nothing because the responsibility for the failure of the demo was not on our side. At the same time, delaying the project delivery date is always unpleasant. We will send the invoice later, there are risks that we will have to rework some tasks later, and the team members have already lost the context and are working on the tasks of other projects. Even more critical that our client got into an unpleasant situation because, without a shelf, it was almost impossible to hold a demonstration successfully.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ru-nVJAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52flyvvp1lm9mtp8y6nv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ru-nVJAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52flyvvp1lm9mtp8y6nv.png" alt="Image description" width="880" height="498"&gt;&lt;/a&gt;&lt;br&gt;
So yeah, we just decided to create a software equivalent of a physical shelf that would do all the same things as a real shelf when finished.&lt;/p&gt;

&lt;p&gt;We started with requirements development because we had to consider many details regarding the work of the shelf. First, we thought about how customers and employees will interact with the shelf. They can accidentally lean on the shelf, which can cause the system to get false statistics. You can also forget to close the refrigerator door, and the temperature will start to rise. After working through possible user scenarios, we decided that the shelf, in this case, could be replaced by a separate (or not separate) application that would be able to easily add a large number of new shelves and send data about the operation of each shelf. We immediately informed the client about this idea and that when we do it, it will be possible to run a demo using our test application only. It will be as relevant as possible to how our admin panel will work with real shelves.&lt;/p&gt;

&lt;p&gt;The work took us a couple of days of teamwork. We simulated the possibility of turning off the shelf or losing connection with it with a probability of 1% every minute. Also, every minute a random amount of goods was "taken" from the shelf, and if the shelf remained less than 30% loaded, with a probability of 50% it was filled again. The typical storage temperature range of the product was from 2 to 6 degrees Celsius, so every minute we increased the temperature with a probability of 5%, and when the temperature exceeded the specified range, the employee received a notification. We also considered the possibility of sending false indicators, which the system had to filter and not take into account in the general work statistics.&lt;/p&gt;

&lt;p&gt;With the help of the software shelf, our team tested all the necessary subsystems and demonstrated the operation of the product to the client. A few days later, our client successfully &lt;a href="https://daiquiri.team/uk/cases/libra-data-shelf?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=libra_hardware_imitation"&gt;held the demonstration&lt;/a&gt; to their customers and fully validated the product hypotheses. When the physical shelf was ready after two weeks, we added it to the system. We ensured that our software counterpart worked similarly to the physical shelf, which again emphasized the correctness of the chosen solution to the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We at Daiquiri Team believe that the development team must be aware of the client's tasks and problems, even if they are outside the competence of the developers. This is how the team gets enough background to understand the entire client's problem and be able to solve it completely. Saying that you can't continue work because third-party system components are not ready is not expedient. Components can always be replaced with another solution.&lt;/p&gt;

&lt;p&gt;As a developer, I truly believe that all developers should care about the end customer of the product, at least partly. I participated in many projects, and I feel it from real life, so if you want to work with me on your IoT project or any other projects — write me directly or contact us on the &lt;a href="https://daiquiri.team/?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=libra_hardware_imitation"&gt;website&lt;/a&gt;, and we will be glad to discuss and solve your problem.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>startup</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to build the SSR web application and mobile apps from the same code base</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Sun, 25 Sep 2022 10:03:17 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/how-to-build-the-ssr-web-application-and-mobile-apps-from-the-same-code-base-4aob</link>
      <guid>https://dev.to/daiquiri_team/how-to-build-the-ssr-web-application-and-mobile-apps-from-the-same-code-base-4aob</guid>
      <description>&lt;p&gt;Server-side rendering improves initial page load speed. The browser receives pre-rendered HTML from the server, so it takes less time to build the page.&lt;/p&gt;

&lt;p&gt;Also, SSR sites play better with SEO. Search engine crawlers grab the site's HTML content without executing JS code. Without SSR, search engines will not index page content and will not find internal links.&lt;/p&gt;

&lt;p&gt;NuxtJS has a &lt;a href="https://nuxtjs.org/docs/concepts/server-side-rendering/" rel="noopener noreferrer"&gt;Server-side rendering&lt;/a&gt; feature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Server-side rendering (SSR), is the ability of an application to contribute by displaying the web-page on the server instead of rendering it in the browser. Server-side sends a fully rendered page to the client.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But you cannot use SSR when you build a static site for the mobile application. Here I'll show how to create an SSR web and cross-platform mobile application from the same code base using CapacitorJS and NuxtJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the app
&lt;/h2&gt;

&lt;p&gt;As an example, we will build a simple app to show local time in &lt;a href="https://en.wikipedia.org/wiki/Kyiv" rel="noopener noreferrer"&gt;Kyiv&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;To test SSR, we need to get data from the API server. We will receive data from &lt;a href="http://worldtimeapi.org/" rel="noopener noreferrer"&gt;worldtimeapi.org&lt;/a&gt; API using the &lt;a href="https://v3.nuxtjs.org/getting-started/data-fetching/#usefetch" rel="noopener noreferrer"&gt;useFetch&lt;/a&gt; method.&lt;/p&gt;

&lt;p&gt;Let's start with the application from the &lt;a href="https://dev.to/daiquiri_team/how-to-create-android-and-ios-apps-from-the-nuxtjs-application-using-capacitorjs-134h"&gt;previous guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Remove &lt;code&gt;app.vue&lt;/code&gt; file and add &lt;code&gt;pages/index.vue&lt;/code&gt; with the following content:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;🇺🇦 Kyiv Time&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
      🕥 Current time: &lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;timeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datetime&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"$nuxt.refresh()"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;🔄&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://worldtimeapi.org/api/timezone/Europe/Kiev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieving current time in &lt;code&gt;const timeData&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;🕥 displaying time on the page.&lt;/li&gt;
&lt;li&gt;🔄 refresh button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run &lt;code&gt;yarn dev&lt;/code&gt; and check &lt;a href="http://localhost:3000/" rel="noopener noreferrer"&gt;http://localhost:3000/&lt;/a&gt; what we have:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_site_ssr_html.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_site_ssr_html.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's look at the page source code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_site_ssr_source.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_site_ssr_source.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In NuxtJS, Server-side rendering is enabled by default. But what will we get if we create a static site for the mobile build? Run &lt;code&gt;yarn generate &amp;amp;&amp;amp; yarn preview&lt;/code&gt; and recheck &lt;a href="http://localhost:3000/" rel="noopener noreferrer"&gt;http://localhost:3000/&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If you try to refresh the page, you will see that time doesn't change. NuxtJS retrieved time from the server during static site generation and saved it in the pre-rendered HTML page.&lt;/p&gt;

&lt;p&gt;Let's add &lt;code&gt;ssr: false&lt;/code&gt; to the &lt;code&gt;nuxt.config.ts&lt;/code&gt; &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineNuxtConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nuxt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// https://v3.nuxtjs.org/api/configuration/nuxt.config&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and rebuild the site with &lt;code&gt;yarn generate &amp;amp;&amp;amp; yarn preview&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now NuxtJS runs HTTP requests on the client side. There is no datetime in the page sources, and datetime changes every time you refresh a page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_site_no_ssr_source.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_site_no_ssr_source.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we must build a web application with SSR and a mobile application in client-only mode. Let's use an environment flag &lt;code&gt;MOBILE_BUILD&lt;/code&gt;: we will build a web application with &lt;code&gt;MOBILE_BUILD=0&lt;/code&gt; and a mobile application with &lt;code&gt;MOBILE_BUILD=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can use a &lt;code&gt;.env&lt;/code&gt; file to store environment variables, and I'll go with &lt;code&gt;export MOBILE_BUILD=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Change &lt;code&gt;nuxt.config.ts&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineNuxtConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nuxt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// https://v3.nuxtjs.org/api/configuration/nuxt.config&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MOBILE_BUILD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run &lt;code&gt;yarn generate &amp;amp;&amp;amp; yarn preview&lt;/code&gt; and verify that the application works in client-only mode.&lt;/p&gt;

&lt;p&gt;Finally, let's build and check the Android application:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

yarn cap sync
yarn cap open android


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run the application on an emulator and check that the refresh button works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_android.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Feugen1j%2Fnuxt-mobile%2Fmain%2Fblog%2Fimg%2F02_android.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🎉 Congratulations, it works (on my laptop, at least 🙂).&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>nuxt</category>
      <category>android</category>
      <category>ssr</category>
    </item>
    <item>
      <title>Makefile for your Django project</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Sun, 18 Sep 2022 16:52:41 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/makefile-for-your-django-project-577n</link>
      <guid>https://dev.to/daiquiri_team/makefile-for-your-django-project-577n</guid>
      <description>&lt;p&gt;During the development process, you often need to run some commands in your terminal: create migrations, run tests, linters, etc. Usually, you execute these commands regularly.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;It's helpful to have shortcuts for such commands. And even better to share them among other developers on the project. For this goal, I use &lt;a href="https://en.wikipedia.org/wiki/Make_(software)#Makefile"&gt;Makefile&lt;/a&gt;. Here is the list of useful commands for a Django project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing dependencies
&lt;/h3&gt;

&lt;p&gt;I'm not a big fan of &lt;a href="https://python-poetry.org/"&gt;Poetry&lt;/a&gt;. It has &lt;a href="https://github.com/dependabot/dependabot-core/issues?q=is%3Aissue+is%3Aopen+poetry+label%3A%22L%3A+python%3Apoetry%22"&gt;some problems&lt;/a&gt; with Dependabot and doesn't play with &lt;a href="https://docs.renovatebot.com/python/"&gt;Renovate&lt;/a&gt; at all. I use &lt;a href="https://github.com/jazzband/pip-tools"&gt;pip-tools&lt;/a&gt; with separate &lt;code&gt;requirements.in&lt;/code&gt; and &lt;code&gt;requirements-dev.in&lt;/code&gt; for local and prod environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;pip-install-dev&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip pip-tools
    pip-sync requirements.txt requirements-dev.txt

&lt;span class="nl"&gt;pip-install&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip pip-tools
    pip-sync requirements.txt

&lt;span class="nl"&gt;pip-update&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip pip-tools
    pip-compile requirements.in
    pip-compile requirements-dev.in
    pip-sync requirements.txt requirements-dev.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pip-install-dev&lt;/code&gt;: Installs all requirements, including local. It is the main command for installing the project's dependencies, so I put this command at the top.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pip-install&lt;/code&gt; - I don't use this command often, but it could be helpful to run your code with production dependencies only. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pip-update&lt;/code&gt; Updates your &lt;code&gt;requirements.txt&lt;/code&gt; and &lt;code&gt;requirements-dev.txt&lt;/code&gt; after you add a new package to the &lt;code&gt;requirements.in&lt;/code&gt; or &lt;code&gt;requirements-dev.in&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Running the project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;server&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    python manage.py migrate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python manage.py runserver

&lt;span class="nl"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    python &lt;span class="nt"&gt;-m&lt;/span&gt; celery &lt;span class="nt"&gt;-A&lt;/span&gt; project_name worker &lt;span class="nt"&gt;--loglevel&lt;/span&gt; info

&lt;span class="nl"&gt;beat&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    python &lt;span class="nt"&gt;-m&lt;/span&gt; celery &lt;span class="nt"&gt;-A&lt;/span&gt; project_name beat &lt;span class="nt"&gt;--loglevel&lt;/span&gt; info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an obvious one. The commands to run local web server and &lt;a href="https://docs.celeryq.dev/en/stable/"&gt;Celery&lt;/a&gt;. Also, I like to apply new migrations automatically when I run the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running linters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;lint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    flake8 palyanytsya
    mypy palyanytsya

&lt;span class="nl"&gt;black&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    python &lt;span class="nt"&gt;-m&lt;/span&gt; black palyanytsya

&lt;span class="nl"&gt;cleanimports&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    isort .
    autoflake &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--remove-all-unused-imports&lt;/span&gt; &lt;span class="nt"&gt;--ignore-init-module-imports&lt;/span&gt; project_name

&lt;span class="nl"&gt;clean-lint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cleanimports black lint&lt;/span&gt;

&lt;span class="nl"&gt;checkmigrations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    python manage.py makemigrations &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nt"&gt;--no-input&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lint&lt;/code&gt;: Runs &lt;a href="https://github.com/PyCQA/flake8"&gt;flake8&lt;/a&gt; linter and &lt;a href="https://github.com/python/mypy"&gt;mypy&lt;/a&gt; type checker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;black&lt;/code&gt;: Automatic code formatting with &lt;a href="https://github.com/psf/black"&gt;Black&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanimports&lt;/code&gt;: runs &lt;a href="https://github.com/PyCQA/isort"&gt;isort&lt;/a&gt; and removes unused imports with &lt;a href="https://github.com/PyCQA/autoflake"&gt;Autoflake&lt;/a&gt;. Be sure to set up &lt;code&gt;profile=black&lt;/code&gt; in isort settings to avoid conflicts with Black. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clean-lint&lt;/code&gt;: run all the stuff above. You can run this command before committing to formatting your code properly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkmigrations&lt;/code&gt;: prevents you from committing model changes without migrations. Really cool stuff!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, I use &lt;code&gt;make lint &amp;amp;&amp;amp; make checkmigrations&lt;/code&gt; in the CI pipeline and in the git pre-commit hook. You can also create a command for setting up such a hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;install-hooks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"make lint &amp;amp;&amp;amp; make checkmigrations"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .git/hooks/pre-commit &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;777 .git/hooks/pre-commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    pytest &lt;span class="nt"&gt;-n&lt;/span&gt; 4 &lt;span class="nt"&gt;-x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs &lt;a href="https://docs.pytest.org/en/7.1.x/"&gt;pytest&lt;/a&gt; in multiprocess mode using &lt;a href="https://github.com/pytest-dev/pytest-xdist"&gt;pytest-xdist&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compiling messages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    python manage.py makemessages &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--ignore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;venv &lt;span class="nt"&gt;--extension&lt;/span&gt; html,py
    python manage.py translate_messages &lt;span class="nt"&gt;-s&lt;/span&gt; en &lt;span class="nt"&gt;-l&lt;/span&gt; uk &lt;span class="nt"&gt;-l&lt;/span&gt; es &lt;span class="nt"&gt;-u&lt;/span&gt;
    python manage.py compilemessages &lt;span class="nt"&gt;--ignore&lt;/span&gt; venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collects all string literals for &lt;a href="https://docs.djangoproject.com/en/4.1/topics/i18n/translation/"&gt;translation&lt;/a&gt;. Also, I use &lt;a href="https://github.com/ankitpopli1891/django-autotranslate"&gt;Django Autotranslate&lt;/a&gt; to translate phrases using Google Translate automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The end
&lt;/h2&gt;

&lt;p&gt;As long as the project goes on, new local commands appear. Maybe, you need to shell into the dev server to run some python code. Or download a fresh database dump (without sensitive data) for local bug reproducing. It's better to keep this knowledge in the codebase, not in your head. Also, you won't need to explain how to do exactly the same operation to your colleagues.&lt;/p&gt;

&lt;p&gt;For more complex tasks, you can even create a shell script and call it from Makefile. So, it would be easier to find this new command for other devs.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>makefile</category>
      <category>automatization</category>
    </item>
    <item>
      <title>Creating a custom page in Django Admin</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Sat, 10 Sep 2022 10:13:06 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/creating-a-custom-page-in-django-admin-4pbd</link>
      <guid>https://dev.to/daiquiri_team/creating-a-custom-page-in-django-admin-4pbd</guid>
      <description>&lt;p&gt;&lt;a href="https://www.djangoproject.com/" rel="noopener noreferrer"&gt;Django&lt;/a&gt; is a great web framework for fast development. &lt;a href="https://docs.djangoproject.com/en/4.1/ref/contrib/admin/" rel="noopener noreferrer"&gt;Django Admin&lt;/a&gt; allows you to manage your data without creating your own views. &lt;/p&gt;

&lt;p&gt;The primary use case of the Admin is &lt;a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete" rel="noopener noreferrer"&gt;CRUD&lt;/a&gt; operations. You can populate your local database with test data or change some real data in the production environment.&lt;/p&gt;

&lt;p&gt;Sometimes, you need some custom interfaces to perform your routines on the project. Fortunately, Django Admin has a lot of room for customization. In this guide, I'll show how to create a custom detail page in Django Admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic setup
&lt;/h2&gt;

&lt;p&gt;Let's start from scratch. I'll use python3.9 in this guide. If you are not interested in the basic details, you can jump to the custom Order page part.&lt;/p&gt;

&lt;p&gt;First, create a virtual environment and install &lt;a href="https://pypi.org/project/Django/" rel="noopener noreferrer"&gt;Django&lt;/a&gt;. Also, we need &lt;a href="https://pypi.org/project/Pillow/" rel="noopener noreferrer"&gt;Pillow&lt;/a&gt; for Django's &lt;code&gt;ImageField&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.9 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv 
&lt;span class="nb"&gt;.&lt;/span&gt; ./venv/bin/activate
pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;django&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;4.1.1 &lt;span class="nv"&gt;Pillow&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;9.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create a new Django project and &lt;code&gt;product&lt;/code&gt; app inside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; django startproject django_admin_example &lt;span class="nb"&gt;.&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; django startapp products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will be a simple shop application with &lt;code&gt;Product&lt;/code&gt;, &lt;code&gt;Order&lt;/code&gt;, and &lt;code&gt;OrderItem&lt;/code&gt; models. &lt;code&gt;Order&lt;/code&gt; can have several &lt;code&gt;OrderItem&lt;/code&gt; and belongs to some &lt;code&gt;User&lt;/code&gt;. Each &lt;code&gt;OrderItem&lt;/code&gt; contains the &lt;code&gt;Product&lt;/code&gt; and &lt;code&gt;count&lt;/code&gt;—quantity of the &lt;code&gt;Product&lt;/code&gt; in the &lt;code&gt;Order&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Add these models in &lt;code&gt;products/models.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_user_model&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;

&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_user_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IntegerField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ImageField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__str__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto_now_add&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__str__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PositiveIntegerField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__str__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's include the &lt;code&gt;product&lt;/code&gt; app in &lt;code&gt;django_admin_example/settings.py&lt;/code&gt;. Also, we need to set &lt;code&gt;MEDIA_URL&lt;/code&gt; and &lt;code&gt;MEDIA_ROOT&lt;/code&gt; for file uploading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# Other apps
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;MEDIA_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;media/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;MEDIA_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;media/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can create and run migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are done with the models. Moving to the Admin part. Create a &lt;code&gt;products/admin.py&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;products.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;


&lt;span class="nd"&gt;@admin.register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderItemInline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TabularInline&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderItem&lt;/span&gt;

&lt;span class="nd"&gt;@admin.register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;inlines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrderItemInline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@admin.register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderItemAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created &lt;a href="https://docs.djangoproject.com/en/4.1/ref/contrib/admin/#inlinemodeladmin-objects" rel="noopener noreferrer"&gt;OrderItemInline&lt;/a&gt; to change order items on the Order form. &lt;/p&gt;

&lt;p&gt;Next, we need to add a URL configuration for the admin site in &lt;code&gt;urls.py&lt;/code&gt;. Also, we want to add media file URLs to serve images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.conf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.conf.urls.static&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;admin/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIA_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIA_ROOT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we need a superuser to login into the admin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py createsuperuser
Username &lt;span class="o"&gt;(&lt;/span&gt;leave blank to use &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;: admin
Email address: admin@test.com
Password: 
Password &lt;span class="o"&gt;(&lt;/span&gt;again&lt;span class="o"&gt;)&lt;/span&gt;: 
Superuser created successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python manage.py runserver&lt;/code&gt;, go to the &lt;a href="http://127.0.0.1:8000/admin/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/admin/&lt;/a&gt;, and log in with the &lt;code&gt;admin&lt;/code&gt; user.&lt;/p&gt;

&lt;p&gt;Now, it's time to fill our database with some dummy data!&lt;/p&gt;

&lt;p&gt;Let's add a &lt;code&gt;customer&lt;/code&gt; user;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6z1mk8tmv2g71n0qqajm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6z1mk8tmv2g71n0qqajm.png" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some product to our shop;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrcynw887hd3xs9jybq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrcynw887hd3xs9jybq1.png" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And create an order for the customer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70fsp7yoen85dz0ddyim.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70fsp7yoen85dz0ddyim.png" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Order Page
&lt;/h2&gt;

&lt;p&gt;Now we are ready to create a page for order summary. We want customer information, a list of ordered products, and a total order sum on this page.&lt;/p&gt;

&lt;p&gt;Let's start with an empty template in &lt;code&gt;products/templates/admin/products/order/detail.html&lt;/code&gt;. Django Admin enforces this structure for the templates: &lt;code&gt;admin/APP_NAME/MODEL_NAME/some_template.html&lt;/code&gt;. So, all custom templates for the &lt;code&gt;Order&lt;/code&gt; model will be in this folder.   &lt;/p&gt;

&lt;p&gt;Then, let's create an &lt;code&gt;OrderDetailView&lt;/code&gt; in &lt;code&gt;admin.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.auth.mixins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PermissionRequiredMixin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.views.generic.detail&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DetailView&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;products.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDetailView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermissionRequiredMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DetailView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;permission_required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products.view_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin/products/order/detail.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this view to the &lt;code&gt;OrderAdmin.get_urls&lt;/code&gt;, create the &lt;code&gt;detail&lt;/code&gt; column and add it to the &lt;code&gt;list_display&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.utils.html&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;format_html&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;products.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;


&lt;span class="nd"&gt;@admin.register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;inlines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrderItemInline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;pk&amp;gt;/detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admin_site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderDetailView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_view&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products_order_detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_urls&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin:products_order_detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;a href=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;📝&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OrderDetailView&lt;/code&gt; will accept &lt;code&gt;pk&lt;/code&gt; as an argument. Also, we wrap the view into &lt;code&gt;admin_site.admin_view&lt;/code&gt;. This wrapper checks that user is logged in, &lt;code&gt;user.is_staff=True&lt;/code&gt;, and enforces CSRF validation.&lt;/p&gt;

&lt;p&gt;Let's check how it looks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2v2s7td95cff1m50315.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2v2s7td95cff1m50315.png" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a template
&lt;/h2&gt;

&lt;p&gt;Now the detail link leads to an empty page. Let's create a real one.&lt;/p&gt;

&lt;p&gt;As docs says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to use the admin layout, extend from admin/base_site.html:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Add this code to the &lt;code&gt;detail.html&lt;/code&gt; and check the page in your browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends 'admin/base_site.html' %}

{% block content %}

    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Order by {{ object.user }} {{ object.created.date }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;dl&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dt&amp;gt;&lt;/span&gt;Name:&lt;span class="nt"&gt;&amp;lt;/dt&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dd&amp;gt;&lt;/span&gt;{{ object.user.get_full_name }}&lt;span class="nt"&gt;&amp;lt;/dd&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dt&amp;gt;&lt;/span&gt;Email:&lt;span class="nt"&gt;&amp;lt;/dt&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dd&amp;gt;&lt;/span&gt;{{ object.user.email }}&lt;span class="nt"&gt;&amp;lt;/dd&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dl&amp;gt;&lt;/span&gt;

{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fythr1ipk744fchrnymbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fythr1ipk744fchrnymbg.png" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page looks like a Django Page, but we are missing some components on the page: breadcrumbs, sidebar, and change password/logout buttons. You can check &lt;a href="https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/base.html" rel="noopener noreferrer"&gt;admin/base.html&lt;/a&gt; and &lt;a href="https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/change_form.html" rel="noopener noreferrer"&gt;admin/change_form.html&lt;/a&gt; to see how they are implemented.&lt;/p&gt;

&lt;p&gt;First, we need a context for the template. Let's override a &lt;code&gt;OrderDetailView.get_context&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDetailView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermissionRequiredMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DetailView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_context_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_context_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will be enough for the sidebar and change password/logout buttons. Now, let's add the breadcrumbs block to the &lt;code&gt;detail.html&lt;/code&gt;. We can take the &lt;code&gt;breadcrumbs&lt;/code&gt; block from the &lt;code&gt;admin/change_form.html&lt;/code&gt; and modify it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% load i18n admin_urls %}

{% block breadcrumbs %}
    &amp;lt;div class="breadcrumbs"&amp;gt;
        &amp;lt;a href="{% url 'admin:index' %}"&amp;gt;{% translate 'Home' %}&amp;lt;/a&amp;gt;
        &amp;amp;rsaquo; &amp;lt;a href="{% url 'admin:app_list' app_label=opts.app_label %}"&amp;gt;{{ opts.app_config.verbose_name }}&amp;lt;/a&amp;gt;
        &amp;amp;rsaquo; &amp;lt;a href="{% url opts|admin_urlname:'changelist' %}"&amp;gt;{{ opts.verbose_name_plural|capfirst }}&amp;lt;/a&amp;gt;
        &amp;amp;rsaquo; {{ object }}
    &amp;lt;/div&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4yyudbuarv5da8pt6me.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4yyudbuarv5da8pt6me.png" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let's add a product table. We need a total amount of Order, so create a &lt;code&gt;total_amount&lt;/code&gt; method for &lt;code&gt;OrderItem&lt;/code&gt; and &lt;code&gt;Order&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderitem_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the table and the total to the &lt;code&gt;detail.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% block content %} 
    ...

    &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 100%; margin-bottom: 10px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;Product&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;Price&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;Count&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;Total&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
        {% for item in object.orderitem_set.all %}
            &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ item.product.image.url }}"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;{{ item.product.title }}&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;${{ item.product.price }}&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;x{{ item.count }}&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;${{ item.total_amount }}&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
        {% endfor %}
        &lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 18px;text-align: end;padding-right: 60px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Total: ${{ object.total_amount }}
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the final result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feszo2hlobluddw64ciqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feszo2hlobluddw64ciqo.png" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The end
&lt;/h2&gt;

&lt;p&gt;Congratulations! We added a custom page to the Django Admin. This is a way you can customize the Admin Django according to your project needs.&lt;/p&gt;

&lt;p&gt;You can find the source code &lt;a href="https://github.com/eugen1j/django-admin-example" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need to build a custom web application, check out our &lt;a href="https://daiquiri.team/services/agile-development?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_admin_1" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to create Android and iOS apps from the NuxtJS application using CapacitorJS</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Wed, 31 Aug 2022 15:02:08 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/how-to-create-android-and-ios-apps-from-the-nuxtjs-application-using-capacitorjs-134h</link>
      <guid>https://dev.to/daiquiri_team/how-to-create-android-and-ios-apps-from-the-nuxtjs-application-using-capacitorjs-134h</guid>
      <description>&lt;p&gt;In this guide, I will show how to wrap an existing &lt;a href="https://v3.nuxtjs.org/" rel="noopener noreferrer"&gt;NuxtJS&lt;/a&gt; web application into Android and iOS mobile apps using &lt;a href="https://capacitorjs.com/" rel="noopener noreferrer"&gt;CapacitorJS&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Capacitor is an open-source native runtime for building Web Native apps. Create cross-platform iOS, Android, and Progressive Web Apps with JavaScript, HTML, and CSS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CapacitorJS runs your static site on a local browser and shows it to the user. Also, it has some interface between native and web contexts for accessing native features.&lt;/p&gt;

&lt;p&gt;But why do you need this if users can access your website from mobile browsers?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's more useful for users. If a user uses your web app frequently, it's easier for them just to click on your app icon than go to the browser and search for your app bookmark or type your app domain name. Both Android and iOS have the feature "Add website to home screen". But a separate native app provides better UX.&lt;/li&gt;
&lt;li&gt;Distribution channel. Users can search for your app in AppStore and PlayMarket. So, it's better to be present on all popular platforms to reach more users.&lt;/li&gt;
&lt;li&gt;Native features. Maybe, your web app doesn't need them. But after you create a mobile version of your app, you can change your mind. At least you can add notifications by &lt;a href="https://firebase.google.com/docs/cloud-messaging" rel="noopener noreferrer"&gt;FCM&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I think we are done with the "why?" part. Let's go to the "how?" part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating dummy NuxtJS application
&lt;/h2&gt;

&lt;p&gt;To simplify this guide, I'll create a new Nuxt application. Feel free to jump to the Installing CapacitorJS section if you already have one.&lt;/p&gt;

&lt;p&gt;So, let's create a nuxt3 application using the &lt;a href="https://v3.nuxtjs.org/getting-started/quick-start/" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;. I'll use &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;NodeJS v16&lt;/a&gt; and &lt;a href="https://yarnpkg.com/" rel="noopener noreferrer"&gt;Yarn v1.22&lt;/a&gt; for installation. &lt;/p&gt;

&lt;p&gt;Open a terminal and run:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn add global nuxi
$ yarn nuxi init nuxt-mobile
$ cd nuxt-mobile
$ yarn install


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here we installed &lt;a href="https://www.npmjs.com/package/nuxi" rel="noopener noreferrer"&gt;nuxi&lt;/a&gt; Nuxt Command Line Interface, created a new project &lt;code&gt;nuxt-mobile&lt;/code&gt;, and installed dependencies from &lt;code&gt;package.json&lt;/code&gt;. You can replace &lt;code&gt;nuxt-mobile&lt;/code&gt; with any name.&lt;/p&gt;

&lt;p&gt;Next, let's change &lt;code&gt;app.vue&lt;/code&gt;. Replace the existing file with the following one: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Nuxt Mobile&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This is a basic example of a mobile app built with NuxtJS and CapacitorJS&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, let's see what we got. Run the development server:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn dev -o


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You will be navigated to the browser. Here you will see your web application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhuzj96x68pus0jn8tixk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhuzj96x68pus0jn8tixk.png" alt="a NuxtJS application in a browser"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's pretty empty, but it will be enough to show how to wrap a web application into mobile apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing CapacitorJS
&lt;/h2&gt;

&lt;p&gt;Now, let's add CapacitorJS to the application. I'll follow instructions from the &lt;a href="https://capacitorjs.com/docs/getting-started" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the root of your app, install Capacitor's main npm depdencies: the core JavaScript runtime and the command line interface (CLI).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn add @capacitor/core
$ yarn add --dev @capacitor/cli


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Then, initialize Capacitor using the CLI questionnaire:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn cap init


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The CLI will ask you questions about your package. You can pick any &lt;code&gt;Name&lt;/code&gt; and &lt;code&gt;Package&lt;/code&gt; (I used &lt;code&gt;NuxtMobile&lt;/code&gt; and &lt;code&gt;com.mycompany.nuxtmobile&lt;/code&gt; values). Then you need to set the &lt;code&gt;Web asset directory&lt;/code&gt; as &lt;code&gt;dist&lt;/code&gt;. This is a destination on &lt;a href="https://nuxtjs.org/docs/concepts/static-site-generation/" rel="noopener noreferrer"&gt;NuxtJS static site&lt;/a&gt;. The CLI will save these answers into &lt;code&gt;capacitor.config.json&lt;/code&gt; and check the file content.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After the Capacitor core runtime is installed, you can install the Android and iOS platforms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn add @capacitor/android @capacitor/ios


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;CapacitorJS uses a static version of the site. So, we need to generate it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn generate 


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we have a static site so that we can initialize iOS and Android applications:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

$ yarn cap add android
$ yarn cap add ios


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These commands create native applications in &lt;code&gt;android&lt;/code&gt; and &lt;code&gt;ios&lt;/code&gt; folders. They clone web content from the &lt;code&gt;dist&lt;/code&gt; directory to &lt;code&gt;ios/App/App/public&lt;/code&gt; and &lt;code&gt;android/app/src/main/assets/public&lt;/code&gt; directories.&lt;/p&gt;

&lt;p&gt;Every time you change your site, you need to run &lt;code&gt;yarn generate&lt;/code&gt; to generate a new static site and &lt;code&gt;npx cap sync&lt;/code&gt; to update the native apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Android app
&lt;/h2&gt;

&lt;p&gt;Let's run the Android application. First, download and install &lt;a href="https://developer.android.com/studio" rel="noopener noreferrer"&gt;AndroidStudio&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;After you finish, return to the console and run &lt;code&gt;yarn cap open android&lt;/code&gt;. This will open &lt;code&gt;android&lt;/code&gt; folder in AndroidStudio. Here you can change your Android files and run the application. &lt;/p&gt;

&lt;p&gt;Let's add an emulator to run. Go to the "Tools" -&amp;gt; "Device Manager" and click "Create Device." &lt;/p&gt;

&lt;p&gt;First, you need to pick "Hardware device." The main difference between different devices is a screen size and availability of PlayMarket. I'll pick the &lt;code&gt;Pixel 2&lt;/code&gt; because it has PlayMarket and a 5.0" screen size. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nqvxoc5qzd4bag0ubur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nqvxoc5qzd4bag0ubur.png" alt="Choosing the Android device for the emulator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, you need to choose the Android OS image. I will use &lt;code&gt;Android API 33&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwt36a8nffunfbpsf12zf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwt36a8nffunfbpsf12zf.png" alt="Choosing Android API for the emulator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, click 'Next' and 'Finish'. After this, you will see a new device on the 'Devices' dropdown. &lt;/p&gt;

&lt;p&gt;Now, you can choose your device and click 'Run' to build and start the application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dnh419smfgfnx8dgzma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dnh419smfgfnx8dgzma.png" alt="Running an Android application"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Android Studio will start the emulator, build, install and run your application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcytxe45yo7o40ci8pe64.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcytxe45yo7o40ci8pe64.png" alt="A capacitor application runs on the Android emulator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are done with Android; let's move to the iOS part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running iOS app
&lt;/h2&gt;

&lt;p&gt;To build the application for iOS, you need a MacBook. If you have Linux or Windows, you can skip this part.&lt;/p&gt;

&lt;p&gt;First, you need XCode to build and run the iOS application. You can get it on &lt;a href="https://apps.apple.com/us/app/xcode/id497799835" rel="noopener noreferrer"&gt;AppStore&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After installation, return to the console and run &lt;code&gt;yarn cap open ios&lt;/code&gt;. This command will open the &lt;code&gt;ios&lt;/code&gt; folder as an XCode project. Here you can change iOS application files and settings; build and run the application.&lt;/p&gt;

&lt;p&gt;Now, we need to pick an emulator device to run the application. I'll use the iPod touch.&lt;/p&gt;

&lt;p&gt;Then click 'Run'. XCode will build the app and run it on the device.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlq56yi8x5y2t5vjomn5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlq56yi8x5y2t5vjomn5.png" alt="XCode, running a project"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdfu20o57xmy1q9ip3vz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdfu20o57xmy1q9ip3vz.png" alt="A capacitor application runs on the iOS emulator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are done with the iOS part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The end
&lt;/h2&gt;

&lt;p&gt;Congratulations! In this guide, we turn a new NuxtJS web application into iOS and Android mobile applications. It's a very basic web app so we wouldn't upload it to stores :) But if you already have a full-featured web application, you can turn it into mobile apps and upload them to PlayMarket and AppStore!&lt;/p&gt;

&lt;p&gt;You can find the source code of the project &lt;a href="https://github.com/eugen1j/nuxt-mobile" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need to build a web or mobile app prototype, check out our &lt;a href="https://daiquiri.team/services/prototyping?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=nuxt_mobile_1" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>nuxt</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>Deploying Django Application on AWS with Terraform. ECS Autoscaling</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Tue, 23 Aug 2022 13:18:22 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-ecs-autoscaling-2oc5</link>
      <guid>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-ecs-autoscaling-2oc5</guid>
      <description>&lt;p&gt;This is the 7th part of the "Deploying Django Application on AWS with Terraform" guide. You can check out the previous steps here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-minimal-working-setup-587g"&gt;Part 1: Minimal Working Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-postgresql-rds-2j0i"&gt;Part 2: Connecting PostgreSQL RDS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-gitlab-cicd-13am"&gt;Part 3: GitLab CI/CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-namecheap-domain-ssl-19a5"&gt;Part 4: Namecheap Domain + SSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/eugen1j/deploying-django-application-on-aws-with-terraform-setting-up-celery-and-sqs-38ik"&gt;Part 5: Celery and SQS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-to-amazon-s3-2a23"&gt;Part 6: Connecting to Amazon S3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this part, we'll make our Django web application scalable using &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-auto-scaling.html" rel="noopener noreferrer"&gt;ECS Autoscaling&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Autoscaling is the ability to increase or decrease the number of running instances. It allows you to handle traffic spikes and save money for low intensive periods of time. &lt;/p&gt;

&lt;p&gt;When you enable autoscaling for ECS service, AWS creates &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html" rel="noopener noreferrer"&gt;Cloudwatch alarms&lt;/a&gt; to determine whether we need to add a new instance or remove a redundant one.&lt;/p&gt;

&lt;p&gt;Let's see how it works in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECS Autoscaling configuration
&lt;/h2&gt;

&lt;p&gt;First, create a new &lt;code&gt;autoscale.tf&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_appautoscaling_target"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;max_capacity&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="nx"&gt;min_capacity&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;resource_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"service/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aws_ecs_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aws_ecs_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;scalable_dimension&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecs:service:DesiredCount"&lt;/span&gt;
  &lt;span class="nx"&gt;service_namespace&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecs"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_appautoscaling_policy"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_web_cpu"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-web-cpu"&lt;/span&gt;
  &lt;span class="nx"&gt;policy_type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TargetTrackingScaling"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_appautoscaling_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_id&lt;/span&gt;
  &lt;span class="nx"&gt;scalable_dimension&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_appautoscaling_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scalable_dimension&lt;/span&gt;
  &lt;span class="nx"&gt;service_namespace&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_appautoscaling_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_namespace&lt;/span&gt;

  &lt;span class="nx"&gt;target_tracking_scaling_policy_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;predefined_metric_specification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;predefined_metric_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ECSServiceAverageCPUUtilization"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;target_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_appautoscaling_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_web&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_target" rel="noopener noreferrer"&gt;AWS AppAutoscaling Target&lt;/a&gt;. We want to scale the &lt;code&gt;prod_backend_web&lt;/code&gt; service by instance count dimension from 1 to 5.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_policy" rel="noopener noreferrer"&gt;AWS AppAutoscaling Policy&lt;/a&gt;. We want to scale &lt;code&gt;prod_backend_web&lt;/code&gt; up when the &lt;code&gt;ECSServiceAverageCPUUtilization&lt;/code&gt; metric exceeds 80%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are ready to apply changes, but first, let's think about load balancer health checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load Balancer Health Checks
&lt;/h2&gt;

&lt;p&gt;Now we have a quire aggressive health checks. If the container fails to respond twice with a timeout of 2 seconds, Load Balancer considers this container unhealthy and removes them.&lt;/p&gt;

&lt;p&gt;It could be an okay solution for the little amount of traffic. But if many requests reach the container and CPU usage goes up to 100%, the container will fail to respond to Health Checks. So, Load Balancer kills them, and we will face an even worse situation: there will be no containers to handle the traffic at all&lt;/p&gt;

&lt;p&gt;The possible solution is to increase health checks &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;unhealthy_threshold&lt;/code&gt;. Thus, there will be more possibility for overloaded containers to survive.&lt;/p&gt;

&lt;p&gt;I think it's not a perfect solution, but it will work for this test. If you know a more elegant way to keep overloaded containers running, feel free to leave a comment.&lt;/p&gt;

&lt;p&gt;Go to the &lt;code&gt;load_balancer.tf&lt;/code&gt; and increase &lt;code&gt;unhealthy_threshold&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, and &lt;code&gt;interval&lt;/code&gt; parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Target group for backend web application&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="nx"&gt;unhealthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;
    &lt;span class="nx"&gt;interval&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's apply our changes and check them at the AWS console.&lt;/p&gt;

&lt;h2&gt;
  
  
  CloudWatch Alarms
&lt;/h2&gt;

&lt;p&gt;First, go to the &lt;a href="https://console.aws.amazon.com/ecs/home" rel="noopener noreferrer"&gt;ECS console&lt;/a&gt; and check the autoscaling policy for the &lt;code&gt;prod_backend_web&lt;/code&gt; ECS Service. Select &lt;code&gt;prod&lt;/code&gt; ECS cluster, select &lt;code&gt;prod-backend-web&lt;/code&gt; service and click "Update". Pass to the step "Set Auto Scaling" and click on the &lt;code&gt;prod-backend-web-cpu&lt;/code&gt; autoscaling policy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fautoscaling_policy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fautoscaling_policy.png" alt="Autoscaling Policy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see that autoscaling becomes effective when average CPU utilization reaches 80%. But what is the condition for scaling down? Let's check CloudWatch alarms associated with this autoscaling policy.&lt;/p&gt;

&lt;p&gt;Go to the &lt;a href="https://console.aws.amazon.com/cloudwatch/home" rel="noopener noreferrer"&gt;CloudWatch console&lt;/a&gt; and look at the alarms. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fcloudwatch_alarms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fcloudwatch_alarms.png" alt="Cloudwatch Alarms"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see that we scale up when the average CPU load exceeds 80% during 3+ minutes. And, we scale down when the average CPU load goes less than 72% for 15 minutes.&lt;/p&gt;

&lt;p&gt;Such specific numbers, but how can we adjust them to our case? For this, you need to create and use custom metrics for alarms with &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_policy#customized_metric_specification" rel="noopener noreferrer"&gt;customized_metric_specification&lt;/a&gt; param in &lt;code&gt;aws_appautoscaling_policy&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Also, you can change &lt;code&gt;AlarmHigh&lt;/code&gt; and &lt;code&gt;AlarmLow&lt;/code&gt; metrics manually in the console. It's not a preferable way to create a repeatable setup, but it's okay for our test. So, I'll change the &lt;code&gt;AlarmLow&lt;/code&gt; metric to 50% and 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Falarm_low_updating.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Falarm_low_updating.png" alt="alarm low updating"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stress Testing
&lt;/h2&gt;

&lt;p&gt;Let's move to the tests. I'll use &lt;a href="https://httpd.apache.org/docs/2.4/programs/ab.html" rel="noopener noreferrer"&gt;ApacheBenchmark&lt;/a&gt; for stress testing. This tool can send a lot of requests to our service, so the CPU load goes up.&lt;/p&gt;

&lt;p&gt;First, ensure that now the web service has only one container running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fecs_web_pre_test.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fecs_web_pre_test.png" alt="only one web container"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, you need to increase the limit of open files with &lt;code&gt;ulimit -n 10000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now we are ready to run the benchmark. We'll use the health-check URL for this test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ab &lt;span class="nt"&gt;-n&lt;/span&gt; 100000 &lt;span class="nt"&gt;-c&lt;/span&gt; 1000 https://api.example53.xyz/health/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;-c 1000&lt;/code&gt; concurrent number of requests, &lt;code&gt;-n 100000&lt;/code&gt; is the total number of requests.&lt;/p&gt;

&lt;p&gt;Check the CloudWatch metrics and ECS Service for the next 10-15 minutes.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fcpu_burst.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fcpu_burst.png" alt="CPU Burst"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, you should see the CPU spike in the charts. After 3 minutes, ECS autoscale starts to spawn new instances.&lt;/p&gt;

&lt;p&gt;Then, the average CPU drops below 80%. There were 3 ECS tasks at this moment of time.&lt;/p&gt;

&lt;p&gt;After some time, CPU load exceeds 80% again, and ECS autoscale creates the 4th instance. You can see them on the ECS console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fecs_web_scale_up.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fecs_web_scale_up.png" alt="ecs web scale up"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, scale up works; let's check scale down. Stop ApacheBenchmark and wait for 10-15 minutes to wait for scale down.&lt;/p&gt;

&lt;p&gt;You'll see how CPU load drops to zero and ECS scales down the web service to 1 instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fcloudwatch_cpu_goes_down.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fcloudwatch_cpu_goes_down.png" alt="cloudwatch cpu goes down"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recheck the ECS console to ensure that we have only one web task running:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fecs_web_post_test.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart7%2Fecs_web_post_test.png" alt="ecs web post test"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, scale down works too. Let's commit and push our changes to the &lt;code&gt;infrastructure&lt;/code&gt; repository. &lt;/p&gt;

&lt;h2&gt;
  
  
  The end
&lt;/h2&gt;

&lt;p&gt;Congratulations! In this part, we added ECS autoscaling for the web service. We increase Health Check timeout and period to prevent killing overloaded containers. Then, we run a stress test and verify that number of instances increases when CPU load goes up and decreases when CPU load goes down.&lt;/p&gt;

&lt;p&gt;You can find the source code of backend and infrastructure projects &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need technical consulting on your project, check out our &lt;a href="https://daiquiri.team/services/technical-consulting?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_aws_7" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>autoscaling</category>
      <category>ecs</category>
    </item>
    <item>
      <title>Deploying Django Application on AWS with Terraform. Connecting to Amazon S3</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Wed, 17 Aug 2022 09:28:54 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-to-amazon-s3-2a23</link>
      <guid>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-to-amazon-s3-2a23</guid>
      <description>&lt;p&gt;This is the sixth part of the "Deploying Django Application on AWS with Terraform" guide. You can check out the previous steps here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-minimal-working-setup-587g"&gt;Part 1: Minimal Working Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-postgresql-rds-2j0i"&gt;Part 2: Connecting PostgreSQL RDS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-gitlab-cicd-13am"&gt;Part 3: GitLab CI/CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-namecheap-domain-ssl-19a5"&gt;Part 4: Namecheap Domain + SSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//..."&gt;Part 5: Celery and SQS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this step, we are going to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an &lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;S3 bucket&lt;/a&gt; for storing media files.&lt;/li&gt;
&lt;li&gt;Add &lt;a href="https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html" rel="noopener noreferrer"&gt;S3 storage&lt;/a&gt; in the Django project.&lt;/li&gt;
&lt;li&gt;Add image upload in the Django admin.&lt;/li&gt;
&lt;li&gt;Add a &lt;a href="https://www.terraform.io/language/settings/backends/s3" rel="noopener noreferrer"&gt;S3 Terraform backend&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About S3
&lt;/h2&gt;

&lt;p&gt;Why do we actually need S3 storage?&lt;/p&gt;

&lt;p&gt;Why do we need S3 storage?&lt;br&gt;
When a user uploads some file to the web server, Django, by default, saves this file in the filesystem. As far as we can have multiple containers for web ECS service, we can run into the situation when container &lt;code&gt;web_1&lt;/code&gt; creates the user file, and container &lt;code&gt;web_2&lt;/code&gt; tries to read them. Moreover, when ECS recreated container &lt;code&gt;web_1&lt;/code&gt;, we will lose the user file.&lt;/p&gt;

&lt;p&gt;So, now we have stateful behavior. To achieve a stateless behavior, we need to store user files in common global storage. S3 is the storage we are looking for.&lt;/p&gt;

&lt;p&gt;In this case, container &lt;code&gt;web_1&lt;/code&gt; creates a new file in an S3 bucket, and container &lt;code&gt;web_2&lt;/code&gt; can read this file.&lt;/p&gt;

&lt;p&gt;Here is an S3 description from AWS:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon Simple Storage Service (Amazon S3) is an object storage service offering industry-leading scalability, data availability, security, and performance. Customers of all sizes and industries can store and protect any amount of data for virtually any use case, such as data lakes, cloud-native applications, and mobile apps. With cost-effective storage classes and easy-to-use management features, you can optimize costs, organize data, and configure fine-tuned access controls to meet specific business, organizational, and compliance requirements.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  File upload to local storage
&lt;/h3&gt;

&lt;p&gt;But first, let's add a file upload in our Django application.&lt;/p&gt;

&lt;p&gt;To work with S3, we need to install &lt;a href="https://django-storages.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;django-storages&lt;/a&gt; and &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html" rel="noopener noreferrer"&gt;boto3&lt;/a&gt; packages. And for handling images in Django, we need the &lt;a href="https://pillow.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;Pillow&lt;/a&gt; library. Add them to &lt;code&gt;requirements.txt&lt;/code&gt; and run &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;boto3==1.24.45
django_storages==1.13.1
pillow==9.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for now, we'll work with Django default &lt;a href="https://docs.djangoproject.com/en/4.1/ref/files/storage/#the-filesystemstorage-class" rel="noopener noreferrer"&gt;FileSystemStorage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's create a new Django application &lt;code&gt;photos&lt;/code&gt; with a &lt;code&gt;Photo&lt;/code&gt; model with &lt;code&gt;image&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ImageField" rel="noopener noreferrer"&gt;ImageField&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Navigate to the &lt;code&gt;django-aws-backend&lt;/code&gt; project and create a new Django application with &lt;code&gt;python manage.py startapp photos&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then add the &lt;code&gt;photos&lt;/code&gt; application in &lt;code&gt;settings.py&lt;/code&gt; at the bottom of &lt;code&gt;INSTALLED_APPS&lt;/code&gt; and specify &lt;a href="https://docs.djangoproject.com/en/4.0/ref/settings/#media-url" rel="noopener noreferrer"&gt;MEDIA_URL&lt;/a&gt; and &lt;a href="https://docs.djangoproject.com/en/4.0/ref/settings/#media-root" rel="noopener noreferrer"&gt;MEDIA_ROOT&lt;/a&gt; settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;photos.apps.PhotosConfig&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;MEDIA_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/media/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MEDIA_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we are ready to create a &lt;code&gt;Photo&lt;/code&gt; model in &lt;code&gt;photos/models.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Photo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ImageField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upload_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photos/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To allow management of  &lt;code&gt;Photo&lt;/code&gt; objects in the admin panel, add &lt;code&gt;PhotoAdmin&lt;/code&gt; class in &lt;code&gt;photos/admin.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;photos.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Photo&lt;/span&gt;


&lt;span class="nd"&gt;@admin.register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PhotoAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, to serve media files by the Django development server, add this code to the &lt;code&gt;django_aws/urls.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django_aws.settings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MEDIA_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MEDIA_ROOT&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.conf.urls.static&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MEDIA_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MEDIA_ROOT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the necessary code is in place. So we can make and apply migrations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py makemigrations photos      
Migrations &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'photos'&lt;/span&gt;:
  photos/migrations/0001_initial.py
    - Create model Photo
&lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py migrate              
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, photos, sessions
Running migrations:
  Applying photos.0001_initial... OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's try to create a new &lt;code&gt;Photo&lt;/code&gt; in Django admin. Run &lt;code&gt;python manage.py runserver&lt;/code&gt; and go to the admin panel &lt;a href="http://127.0.0.1:8000/admin/photos/photo/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/admin/photos/photo/&lt;/a&gt;. Click "Add Photo", enter a title, and pick any image from your filesystem. Then, click "Save and continue editing".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fadd_local_file.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fadd_local_file.png" alt="New Photo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Django will save an image to the local filesystem. Verify that you can click on the "current image" and see it in the browser. You can also check it in the &lt;code&gt;django-aws-backend/media/photos&lt;/code&gt; folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Django S3 settings
&lt;/h3&gt;

&lt;p&gt;We've successfully uploaded an image to the local filesystem. I prefer to use &lt;code&gt;FileSystemStorage&lt;/code&gt; for local development. It allows me to work and run tests without the internet connection. &lt;/p&gt;

&lt;p&gt;But for the production environment, we need to provide S3 settings. So let's add to &lt;code&gt;settings.py&lt;/code&gt; the following settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFAULT_FILE_STORAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEFAULT_FILE_STORAGE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.core.files.storage.FileSystemStorage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_STORAGE_BUCKET_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_STORAGE_BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_S3_REGION_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_S3_REGION_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_S3_ENDPOINT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_S3_ENDPOINT_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_S3_FILE_OVERWRITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.djangoproject.com/en/4.0/ref/settings/#default-file-storage" rel="noopener noreferrer"&gt;DEFAULT_FILE_STORAGE&lt;/a&gt;. We keep &lt;code&gt;FileSystemStorage&lt;/code&gt; for local development and set &lt;a href="https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings" rel="noopener noreferrer"&gt;S3Boto3Storage&lt;/a&gt; for production.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; - AWS access key, as a string. We'll create a separate user and access key for accessing s3 storage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_STORAGE_BUCKET_NAME&lt;/code&gt; - the name of S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_S3_REGION_NAME&lt;/code&gt; - AWS region. I'll use the same &lt;code&gt;us-east-2&lt;/code&gt; region; you can specify yours.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_S3_ENDPOINT_URL&lt;/code&gt; - URL for connecting to S3.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_S3_FILE_OVERWRITE&lt;/code&gt; - If the file with the specified name already exists, &lt;code&gt;django-storages&lt;/code&gt; will append extra characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are done with the Django part. The application is ready to work with S3. Commit and push the changes, and ensure that CI/CD is passed successfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating S3 bucket
&lt;/h3&gt;

&lt;p&gt;Move to the Terraform project. Let's create an S3 bucket. First, we need to specify the bucket name in &lt;code&gt;variables.tf&lt;/code&gt;. The name should be unique in your AWS region.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# S3&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_media_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3 Bucket for production media files"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-media-427861343"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create a new &lt;code&gt;s3.tf&lt;/code&gt; file in the &lt;code&gt;django-aws-infrastructure&lt;/code&gt; project with the following content and run &lt;code&gt;terraform apply&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"prod_media"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;
  &lt;span class="nx"&gt;acl&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public-read"&lt;/span&gt;

  &lt;span class="nx"&gt;cors_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_origins&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;expose_headers&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ETag"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;max_age_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PublicReadGetObject"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/*"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"prod_media_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-media-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user_policy"&lt;/span&gt; &lt;span class="s2"&gt;"prod_media_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"s3:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/*"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_access_key"&lt;/span&gt; &lt;span class="s2"&gt;"prod_media_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new S3 bucket. We allowed public access to all files in this bucket and specified &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS&lt;/a&gt; rules to allow the browser to load files for any origin (and for our domain as well).&lt;/li&gt;
&lt;li&gt;A new &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html" rel="noopener noreferrer"&gt;IAM user&lt;/a&gt; with the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html" rel="noopener noreferrer"&gt;IAM policy&lt;/a&gt; to connect to the S3 bucket.&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html" rel="noopener noreferrer"&gt;IAM Access Key&lt;/a&gt; for the user to use in the Django application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's check our bucket in &lt;a href="https://s3.console.aws.amazon.com/s3/home" rel="noopener noreferrer"&gt;AWS S3 Console&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Faws_s3_console.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Faws_s3_console.png" alt="AWS S3 Console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we are ready to pass S3 credentials to the ECS services. Add variables in &lt;code&gt;ecs.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;container_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;

    &lt;span class="nx"&gt;s3_media_bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;
    &lt;span class="nx"&gt;s3_access_key&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_access_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;s3_secret_key&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_access_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_media_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use these variables for defining environment variables in &lt;code&gt;templates/backend_container.json.tpl&lt;/code&gt; and apply changes with &lt;code&gt;terraform apply&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEFAULT_FILE_STORAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storages.backends.s3boto3.S3Boto3Storage"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_ACCESS_KEY_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3_access_key}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_SECRET_ACCESS_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3_secret_key}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_STORAGE_BUCKET_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3_media_bucket}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_S3_REGION_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${region}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_S3_ENDPOINT_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://${s3_media_bucket}.s3.${region}.amazonaws.com/"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the deployment new version of ECS services. Then, go to the admin panel of the deployed application and try to add a new photo. Fill the fields and click "Save and continue editing." Then click the current image URL to ensure your photo is uploaded to S3 correctly. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fadd_s3_file.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fadd_s3_file.png" alt="Add photo s3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, check your file in the S3 console: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Faws_s3_console_with_a_file.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Faws_s3_console_with_a_file.png" alt="AWS S3 Console with a file"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we successfully added file uploading to S3 for our Django application. Now the application is stateless so that we can scale without problems. &lt;/p&gt;

&lt;p&gt;The last feature I want to add to our Terraform project is an &lt;a href="https://www.terraform.io/language/settings/backends/s3" rel="noopener noreferrer"&gt;S3 backend&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform S3 backend
&lt;/h2&gt;

&lt;p&gt;Why do we actually need an S3 backend for Terraform? &lt;/p&gt;

&lt;p&gt;Now Terraform stores the state of our infrastructure locally in the &lt;code&gt;terraform.tfstate&lt;/code&gt; file. So, you can apply changes from your local machine only and cannot collaborate with other developers. Also, if you decide to add CI/CD to the Terraform project, your CD runner will have no information about the current infrastructure state.&lt;/p&gt;

&lt;p&gt;The solution to these problems is a remote backend. Terraform will store &lt;code&gt;terraform.tfstate&lt;/code&gt; file in S3 bucket. So, you can apply changes from different environments, including CD runners. &lt;/p&gt;

&lt;p&gt;So, let's go to the AWS S3 Console and &lt;a href="https://s3.console.aws.amazon.com/s3/bucket/create" rel="noopener noreferrer"&gt;create an S3 bucket&lt;/a&gt; in your region. You can choose any name for your bucket, and I'll go with &lt;code&gt;terraform-427861343&lt;/code&gt;. Be sure to disable public access so that nobody can know your infrastructure state. Also, it's better to enable versioning on this bucket. It allows you to roll back to the previous version in case of some problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fcreate_terraform_bucker.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fcreate_terraform_bucker.png" alt="Create Terraform bucket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we can specify the S3 backend in &lt;code&gt;providers.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-427861343"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-2"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and initialize the provider with &lt;code&gt;terraform init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Terraform will ask if you want to copy the existing state to the new backend. If you say no, Terraform will create an empty state in the S3 backend. We want to preserve the state of the infrastructure, so you need to respond &lt;code&gt;yes&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found &lt;span class="k"&gt;while &lt;/span&gt;migrating the previous &lt;span class="s2"&gt;"local"&lt;/span&gt; backend to the
  newly configured &lt;span class="s2"&gt;"s3"&lt;/span&gt; backend. No existing state was found &lt;span class="k"&gt;in &lt;/span&gt;the newly
  configured &lt;span class="s2"&gt;"s3"&lt;/span&gt; backend. Do you want to copy this state to the new &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  backend? Enter &lt;span class="s2"&gt;"yes"&lt;/span&gt; to copy and &lt;span class="s2"&gt;"no"&lt;/span&gt; to start with an empty state.

  Enter a value: &lt;span class="nb"&gt;yes


&lt;/span&gt;Successfully configured the backend &lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Terraform will automatically
use this backend unless the backend configuration changes.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, let's check that we've migrated successfully. Run &lt;code&gt;terraform apply&lt;/code&gt; to verify that our infrastructure matches the configuration. You should see the message&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also, check your S3 bucket. You should see a new &lt;code&gt;terraform&lt;/code&gt; file here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fterraform_state_in_s3_bucket.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart6%2Fterraform_state_in_s3_bucket.png" alt="Terraform state in S3 bucket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we successfully move our Terraform state to S3. Commit and push the changes in the Terraform project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The end
&lt;/h2&gt;

&lt;p&gt;Congratulations! In this part, we added image uploading to S3 for our Django application. Also, we moved Terraform backend to S3. If you want more insight into application state, resource usage, and changes, you can use &lt;a href="https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs" rel="noopener noreferrer"&gt;Spacelift Terraform provider&lt;/a&gt;. Check how Spacelift can improve your workflow &lt;a href="https://spacelift.io/blog/how-specialized-solution-can-improve-your-iac" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can find the source code of backend and infrastructure projects &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need technical consulting on your project, check out our &lt;a href="https://daiquiri.team/services/technical-consulting?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_aws_6" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>django</category>
      <category>aws</category>
      <category>terraform</category>
      <category>s3</category>
    </item>
    <item>
      <title>How to become a better Django Developer by doing a pet project?</title>
      <dc:creator>Yevhenii Kosmak</dc:creator>
      <pubDate>Tue, 16 Aug 2022 16:23:44 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/how-to-become-a-better-django-developer-by-doing-a-pet-project-244l</link>
      <guid>https://dev.to/daiquiri_team/how-to-become-a-better-django-developer-by-doing-a-pet-project-244l</guid>
      <description>&lt;p&gt;My name is Zhenya Kosmak. Now I work as a product manager, but I have 3+ years of quite serious commercial experience using Django. And even now, I strive to code for at least 10 hours weekly. I deeply believe the best way to master any framework, Django as well, is to build a truly big pet project. So a few bits of my recent pet project experience.&lt;/p&gt;

&lt;p&gt;The pet project's name is &lt;a href="https://palianytsia.app/"&gt;Palianytsia&lt;/a&gt;. It's a tool for enriching Ukrainian vocabulary and, generally, studying Ukrainian. Now, 2 months after project public launch, we have 200+ DAU (daily active users) across all clients (web app, iOS, and Android apps) and a sufficient audience (10K+ followers on &lt;a href="https://www.instagram.com/palianytsia.app/"&gt;Instagram&lt;/a&gt;). The back-end of this product is entirely on Django.&lt;/p&gt;

&lt;p&gt;It must be noted that a great developer in the modern world couldn’t be imagined as a single player. Many tough challenges show up, particularly in teamwork. So it would be a rather more quality experience if you start your pet project with a team containing competent people for such roles as product owner, designer, and project manager.&lt;/p&gt;

&lt;p&gt;Your pet project should solve a sufficient number of most standard back-end tasks. The tasks have to be real, not synthetical. Here are examples of how you can set them up. &lt;/p&gt;

&lt;h2&gt;
  
  
  Main feature
&lt;/h2&gt;

&lt;p&gt;In our case, it was a search mechanism on top of ElasticSearch. We have a database of sentences from different sources, and the task of this API endpoint is to return the most relevant sentences for each query. We have additional aspects on this. The sentences must show different contexts in which the user's input was used. Also, the search results should come from various sources in order to include principally different ways to use the search phrase.&lt;/p&gt;

&lt;p&gt;This task teaches you how to work with ElasticSearch, and customize its ordering. Also, you practice using DRF to build quality API endpoints here. If you want to obtain excellent documentation of such endpoints, you might use &lt;code&gt;drf-spectacular&lt;/code&gt; or else as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data collection
&lt;/h2&gt;

&lt;p&gt;Django developer must be a Python professional as well. Not all the everyday tasks are covered with some Django libraries, but they still have to be solved. HTTP scraping is one of the most standard ones.&lt;/p&gt;

&lt;p&gt;We needed to collect articles from 40+ websites to provide users with comprehensive examples of each word or phrase usage. So we've built some abstract parser classes for most routine needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BaseParser. We define general parsing logic here.&lt;/li&gt;
&lt;li&gt;BaseFeedPaginationParser. We use it to collect article URLs when the website's feeds are paginated, i.e., the URLs of the feed look like &lt;code&gt;https://example.com/feed/&amp;lt;page&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;BaseFeedCrawlerParser. On some sites, there were no paginated URLs; they had only infinite scroll or "Next page" URLs on each page. This parser works with such websites. Also, we use it for Wikipedia.&lt;/li&gt;
&lt;li&gt;BaseArticleParser. This guy is used for collecting the content of each article.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Such class hierarchy was a definitely productive solution. After finishing work on these classes, developing a parser was mainly like writing a config file. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DetectorMediaBaseFeedParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseFeedParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;feed_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;archive/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;date_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DetectorMediaBaseArticleParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseArticleParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;paragraphs_selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#artelem &amp;gt; p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_paragraphs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HtmlElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="c1"&gt;# Current markup
&lt;/span&gt;        &lt;span class="n"&gt;paragraphs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;extract_paragraphs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;paragraphs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;paragraphs&lt;/span&gt;

        &lt;span class="c1"&gt;# Old br-only markup
&lt;/span&gt;        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cssselect_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#artelem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;paragraphs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split_br_into_paragraphs_lxml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;paragraphs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can only guess what a mess we could get if we developed each parser separately. It's also an excellent practice for &lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;asyncio&lt;/code&gt;, or anything else you prefer to get the data from websites.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9q9uq56jnlu3gs7ooyfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9q9uq56jnlu3gs7ooyfn.png" alt="When you’ve made 40 similar scrapers from scratch…" width="800" height="452"&gt;&lt;/a&gt;When you’ve made 40 similar scrapers from scratch…&lt;/p&gt;

&lt;p&gt;This task also teaches you how to orchestrate those parsers. We had to gather millions of articles based on our estimates, and we wanted to be able to collect all data from all sites in 24 hours. So we have to use a tasks queue to ensure the solution is efficient and scalable. The most common approach on the Django stack is to use &lt;code&gt;celery&lt;/code&gt;, but we prefer &lt;code&gt;dramatiq&lt;/code&gt; as the most lightweight and still productive one. Bringing such a solution to a stable and scalable level is also a very effective way to improve your coding skills.&lt;/p&gt;

&lt;p&gt;Last but not least, we need a state here. We have to save each gathered URL and article content after scraping, so we wouldn't lose the work results if our server crashes. And the content is stored in S3 Wasabi cloud storage. Here is your practice with PostgreSQL or any other RDBMS you'd like, and S3 as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data processing
&lt;/h2&gt;

&lt;p&gt;Okay, we have articles; what's next? We need a database of Ukrainian sentences split properly, with filtered artifacts (such as BB-code parts or broken HTML, which occurs on most websites), normalized (we don't need repeated spaces or else). As input, we have a PostgreSQL table with articles; each article is stored on S3 storage and split into paragraphs. So we've got such tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Split a paragraph into a list of sentences. The job looks easier than it is. No way it's something like &lt;code&gt;paragraph.split('.')&lt;/code&gt;, it's way harder. For some direction, you can look at &lt;a href="https://stackoverflow.com/questions/4576077/how-can-i-split-a-text-into-sentences"&gt;this&lt;/a&gt; StackOverflow question.&lt;/li&gt;
&lt;li&gt;Skip all paragraphs with mostly non-Ukrainian content.&lt;/li&gt;
&lt;li&gt;Filter out trash content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Let's add some 3rd-party tools
&lt;/h2&gt;

&lt;p&gt;Sometimes when you want to say something in one language, you can only recall the word or phrase in another. So let's add Google Translate so that our users would be able to enter queries using any language, and we will search for translation in our database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnitfhanqtxbbn0tse1lq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnitfhanqtxbbn0tse1lq.png" alt='What is Ukrainian for "deeply concerned"?' width="800" height="452"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;It's a nice opportunity to practice using dependencies (&lt;code&gt;google-cloud-translate&lt;/code&gt;), storing credentials, and even developing a cache mechanic. The last one would be helpful to decrease the number of requests sent to the API so that we will spend less. By the way, if you send less than 500K characters monthly, you wouldn't even be charged with Google Cloud cause it's free under that limit.&lt;/p&gt;

&lt;p&gt;And one more tricky task. The user searches a query. Should you or shouldn't you translate it? There are several ways to solve this issue. We decided to go this way: we stem and normalize all the words from the query, then we find all of them in our dictionary of Ukrainian words. We definitely shouldn't go for a translation if we found all of them. Else way, we better get it.&lt;/p&gt;

&lt;h2&gt;
  
  
  User-related mechanics
&lt;/h2&gt;

&lt;p&gt;We need authentication, authorization, registration, password recovery — all that boring stuff.&lt;/p&gt;

&lt;p&gt;It's a good chance to practice &lt;code&gt;django-allauth&lt;/code&gt;, &lt;code&gt;dj-rest-auth&lt;/code&gt;, &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt;. Remember such things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users shouldn't be logged out for no reason, so cookies shouldn't expire in one hour,&lt;/li&gt;
&lt;li&gt;Their credentials must be safe, so that should be HTTP-only cookies and data kept secure on the server side,&lt;/li&gt;
&lt;li&gt;All errors must be processed intuitively and logically, so the front-end part will be easy to develop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developing 3rd-party authentications is also a good idea for new technical experience. Adding Google or Facebook as sign-up methods might be a more intriguing task than you'd imagine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should we do some automated tests?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vneyudie1z33b0r9jp5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vneyudie1z33b0r9jp5.png" alt="This many tests we’ve made!" width="800" height="452"&gt;&lt;/a&gt;This many tests we’ve made!&lt;/p&gt;

&lt;p&gt;Of course, we should! But please don't do it like your home assignment at university. Choose the most vulnerable parts of projects for future developer mistakes and cover them up. In our case, we made such:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ElasticSearch test for disk usage, RAM, and response speed. Actually, we did this before all else in this project as a part of the proof-of-concept stage. We filled the ElasticSearch DB with dummy data and used &lt;code&gt;ApacheBench&lt;/code&gt;, and some write-only Python scripts to test the performance on the production-level server.&lt;/li&gt;
&lt;li&gt;Performance tests on the primary endpoint to ensure we'll endure the load. These tests were added to the stable code base so that we could rerun the test when needed.&lt;/li&gt;
&lt;li&gt;Unit tests for parsers, text processing mechanics, and string utilities. So that we can be sure that our technical solutions would be changed only in a conscious way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Let's summarize. On such a pet project, you can effectively learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General business logic development. When you have a real-world task, the coding experience becomes more fluent.&lt;/li&gt;
&lt;li&gt;Work with mainly used databases. Any project relies on them, so if your task is challenging enough, you may learn those well.&lt;/li&gt;
&lt;li&gt;Task queues. They become vital just as you encounter any IO-bound, CPU-bound or high-loaded objective. Choose high restrictions, and you learn it well for sure.&lt;/li&gt;
&lt;li&gt;3rd-parties. They are an integral part of any product. The more you learn, the better you're ready for the next ones.&lt;/li&gt;
&lt;li&gt;Network libraries. Not to say vital, but an omnipresent piece of knowledge you'd definitely need. Any pet project will acquire your attention on this point.&lt;/li&gt;
&lt;li&gt;Tests. Not homework, but real salvation of doing error twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's almost the same as spending a year on a commercial project. But you can do it yourself, and if you are patient and stubborn enough, you can master it. So think about it :)&lt;/p&gt;

&lt;p&gt;And the last thing. If you are thinking about something bigger than a pet project, you can &lt;a href="https://www.linkedin.com/in/yevgeniy-kosmak/"&gt;talk with me directly&lt;/a&gt; or &lt;a href="https://daiquiri.team/contact-us?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_and_pet_project"&gt;check out our cases on Daiquiri Team&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Deploying Django Application on AWS with Terraform. Setting up Celery and SQS</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Fri, 12 Aug 2022 12:31:00 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-setting-up-celery-and-sqs-38ik</link>
      <guid>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-setting-up-celery-and-sqs-38ik</guid>
      <description>&lt;p&gt;This is the fifth part of the "Deploying Django Application on AWS with Terraform" guide. You can check out the previous steps here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-minimal-working-setup-587g"&gt;Part 1: Minimal Working Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-postgresql-rds-2j0i"&gt;Part 2: Connecting PostgreSQL RDS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-gitlab-cicd-13am"&gt;Part 3: GitLab CI/CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-namecheap-domain-ssl-19a5"&gt;Part 4: Namecheap Domain + SSL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this step, we are going to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a &lt;a href="https://github.com/celery/celery" rel="noopener noreferrer"&gt;Celery&lt;/a&gt; + SQS setup for local development.&lt;/li&gt;
&lt;li&gt;Create a periodic task using &lt;a href="https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html" rel="noopener noreferrer"&gt;Celery Beat&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add create an &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;SQS&lt;/a&gt; instance on AWS.&lt;/li&gt;
&lt;li&gt;Deploy &lt;a href="https://docs.celeryq.dev/en/stable/userguide/workers.html" rel="noopener noreferrer"&gt;worker&lt;/a&gt; and &lt;a href="https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html" rel="noopener noreferrer"&gt;beat&lt;/a&gt; &lt;a href="https://aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;ECS&lt;/a&gt; services on AWS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About Celery and SQS
&lt;/h2&gt;

&lt;p&gt;As docs says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Celery is a simple, flexible, and reliable distributed system to process vast amounts of messages, while providing operations with the tools required to maintain such a system. It’s a task queue with focus on real-time processing, while also supporting task scheduling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, you can run your long-running, CPU-bound, IO-bound tasks in separate docker containers. Your web server can schedule some tasks, and Celery will pick and execute them. &lt;/p&gt;

&lt;p&gt;Web server and Celery communicate via some backend. It could be &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; or &lt;a href="https://www.rabbitmq.com/" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;. But we use AWS as a cloud provider. So, we can use the &lt;a href="https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html#sqs" rel="noopener noreferrer"&gt;SQS backend&lt;/a&gt;. Celery docs says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you already integrate tightly with AWS, and are familiar with SQS, it presents a great option as a broker. It is extremely scalable and completely managed, and manages task delegation similarly to RabbitMQ.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can check more info about SQS &lt;a href="https://aws.amazon.com/sqs/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Running SQS locally
&lt;/h3&gt;

&lt;p&gt;SQS is a managed solution by AWS. And we could create a separate SQS instance for development purposes. But I want to run all things locally to be able to work without the Internet connection. Also, it's a good practice to run unit and integration tests without Internet access.&lt;/p&gt;

&lt;p&gt;To run SQS locally, we will use &lt;a href="https://hub.docker.com/r/softwaremill/elasticmq-native/" rel="noopener noreferrer"&gt;softwaremill/elasticmq-native&lt;/a&gt; Docker image.&lt;/p&gt;

&lt;p&gt;Go to the &lt;code&gt;django-aws-backend&lt;/code&gt; folder and add a new service to &lt;code&gt;docker-compose.yml&lt;/code&gt; in your Django project:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;...&lt;/span&gt;

  &lt;span class="s"&gt;sqs&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;softwaremill/elasticmq-native:latest"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9324:9324"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9325:9325"&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run &lt;code&gt;docker-compose up -d&lt;/code&gt; to run the SQS container. Then check &lt;a href="http://127.0.0.1:9325/" rel="noopener noreferrer"&gt;http://127.0.0.1:9325/&lt;/a&gt; in your browser to see the SQS management panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Flocal_sqs_empty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Flocal_sqs_empty.png" alt="Local empty SQS"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, check &lt;a href="http://127.0.0.1:9324/" rel="noopener noreferrer"&gt;http://127.0.0.1:9324/&lt;/a&gt; URL to ensure that SQS API is working. You will see an error XML output like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;ErrorResponse&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://queue.amazonaws.com/doc/2012-11-05/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Error&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Type&amp;gt;&lt;/span&gt;Sender&lt;span class="nt"&gt;&amp;lt;/Type&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;MissingAction&lt;span class="nt"&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Message&amp;gt;&lt;/span&gt;MissingAction; see the SQS docs.&lt;span class="nt"&gt;&amp;lt;/Message&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Detail/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Error&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;RequestId&amp;gt;&lt;/span&gt;00000000-0000-0000-0000-000000000000&lt;span class="nt"&gt;&amp;lt;/RequestId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ErrorResponse&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, we are ready to add Celery to our Django project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Celery to Django project
&lt;/h3&gt;

&lt;p&gt;Let's add the Celery package to &lt;code&gt;requirements.txt&lt;/code&gt; and run &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;. Be sure you activated &lt;code&gt;venv&lt;/code&gt; before. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

celery[sqs]==5.2.6


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then, create a new file &lt;code&gt;django_aws/celery.py&lt;/code&gt; with the following content:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Celery&lt;/span&gt;

&lt;span class="c1"&gt;# Set the default Django settings module for the 'celery' program.
&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django_aws.settings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Celery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django_aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config_from_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.conf:settings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CELERY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load task modules from all registered Django apps.
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autodiscover_tasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and add to the &lt;code&gt;django_aws/settings.py&lt;/code&gt; these lines:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="n"&gt;CELERY_BROKER_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CELERY_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqs://localhost:9324&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CELERY_TASK_DEFAULT_QUEUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CELERY_TASK_DEFAULT_QUEUE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CELERY_BROKER_TRANSPORT_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here we initialized the Celery app in &lt;code&gt;django_aws/celery.py&lt;/code&gt;. We will use this app to specify and schedule tasks for Celery. Also, we provided connection parameters in &lt;code&gt;django_aws/settings.py&lt;/code&gt;. As default values, we set our local setup. For production, we can pass parameters via environment variables.&lt;/p&gt;

&lt;p&gt;Now, we are ready to create and run our first task. Let's create &lt;code&gt;django_aws/tasks.py&lt;/code&gt; with the following code:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django_aws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt;


&lt;span class="nd"&gt;@celery.app.task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;web_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting web task...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Done web task.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;web_task&lt;/code&gt; will run for 10 seconds and put messages in the log stream at the start and the end of execution.&lt;/p&gt;

&lt;p&gt;Now, we need to add a way to add this task to the queue. Let's create a &lt;code&gt;django_aws/views.py&lt;/code&gt; with the following &lt;code&gt;view&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django_aws.tasks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;web_task&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_web_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;web_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Add this view to &lt;code&gt;urls.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django_aws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create-task&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_web_task&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, if we hit URL &lt;a href="http://127.0.0.1:8000/create-task" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/create-task&lt;/a&gt;, the &lt;code&gt;create_web_task&lt;/code&gt; view will add a new task to the local SQS. Start the local web server with &lt;code&gt;python manage.py runserver&lt;/code&gt;, hit this URL several times, and look at the SQS admin page &lt;a href="http://127.0.0.1:9325/" rel="noopener noreferrer"&gt;http://127.0.0.1:9325/&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Flocal_sqs_full.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Flocal_sqs_full.png" alt="Local SQS with 3 messages"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we successfully add tasks to the queue. Now, let's execute them with celery. Run &lt;code&gt;celery -A django_aws worker --loglevel info&lt;/code&gt; to start the worker process. The worker will immediately pick tasks from the queue and execute them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fcelery_running_logs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fcelery_running_logs.png" alt="Celery running logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stop the &lt;code&gt;celery&lt;/code&gt; process.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you run into the problem &lt;code&gt;ImportError: The curl client requires the pycurl library&lt;/code&gt;, check out my &lt;a href="https://stackoverflow.com/a/72087928/8153147" rel="noopener noreferrer"&gt;post on StackOverflow&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also, we need to add some libraries to &lt;code&gt;Dockerfile&lt;/code&gt; for compiling the &lt;code&gt;pycurl&lt;/code&gt; in the docker image. Replace &lt;code&gt;Dockerfile&lt;/code&gt; with the next one:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.10-slim-buster&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED 1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE 1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DEBIAN_FRONTEND noninteractive&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update  &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        build-essential &lt;span class="se"&gt;\
&lt;/span&gt;        libssl-dev &lt;span class="se"&gt;\
&lt;/span&gt;        libcurl4-openssl-dev &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;gunicorn&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;20.1.0

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt /&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /requirements.txt

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;./manage.py collectstatic &lt;span class="nt"&gt;--noinput&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Adding Celery beat
&lt;/h3&gt;

&lt;p&gt;Now, let's create a periodic task using &lt;a href="https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html" rel="noopener noreferrer"&gt;Celery Beat&lt;/a&gt;. We will add a simple task like &lt;code&gt;create_web_task&lt;/code&gt; and schedule it for execution once a minute. For this, let's add &lt;code&gt;beat_task&lt;/code&gt; to &lt;code&gt;tasks.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="nd"&gt;@celery.app.task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;beat_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting beat task...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Done beat task.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then, add the &lt;a href="https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-schedule" rel="noopener noreferrer"&gt;CELERY_BEAT_SCHEDULE&lt;/a&gt; setting in &lt;code&gt;settings.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;CELERY_BEAT_SCHEDULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;beat_task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django_aws.tasks.beat_task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and run the beat process &lt;code&gt;celery -A django_aws beat --loglevel info&lt;/code&gt;. Every minute &lt;code&gt;beat&lt;/code&gt; process adds a new task to SQS. Check &lt;a href="http://127.0.0.1:9325/" rel="noopener noreferrer"&gt;http://127.0.0.1:9325/&lt;/a&gt; to see them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fcelery_beat_running_logs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fcelery_beat_running_logs.png" alt="Celery Beat running logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wait for several tasks in queue, stop the &lt;code&gt;beat&lt;/code&gt; process and run the worker again &lt;code&gt;celery -A django_aws worker --loglevel info&lt;/code&gt;. The worker will process &lt;code&gt;beat_task&lt;/code&gt; tasks, and you will see the logs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:13:59,088: INFO/MainProcess] Task django_aws.tasks.beat_task[4189aa07-b75e-4743-94e0-2a0c3b84443a] received
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:13:59,089: INFO/MainProcess] Task django_aws.tasks.beat_task[0de67363-2e2a-421c-9630-1c6c7c685382] received
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:13:59,095: INFO/ForkPoolWorker-1] Starting beat task...
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:13:59,095: INFO/ForkPoolWorker-8] Starting beat task...
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:14:09,096: INFO/ForkPoolWorker-1] Done beat task.
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:14:09,096: INFO/ForkPoolWorker-8] Done beat task.
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:14:09,097: INFO/ForkPoolWorker-1] Task django_aws.tasks.beat_task[0de67363-2e2a-421c-9630-1c6c7c685382] succeeded &lt;span class="k"&gt;in &lt;/span&gt;10.002475121000316s: None
&lt;span class="o"&gt;[&lt;/span&gt;2022-08-04 11:14:09,097: INFO/ForkPoolWorker-8] Task django_aws.tasks.beat_task[4189aa07-b75e-4743-94e0-2a0c3b84443a] succeeded &lt;span class="k"&gt;in &lt;/span&gt;10.002584206999018s: None


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So, we successfully run Celery worker and beat processes using local SQS. Let's add the &lt;code&gt;celerybeat-schedule&lt;/code&gt; file to &lt;code&gt;.gitignore&lt;/code&gt;, commit and push our changes. Ensure that CI/CD passed successfully.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fci_cd_success.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fci_cd_success.png" alt="CI/CD success"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are done with Django part, let's move to the AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to AWS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating AWS SQS instance and user
&lt;/h3&gt;

&lt;p&gt;Move to the &lt;code&gt;django-aws-infrastructure&lt;/code&gt; folder, create a &lt;code&gt;sqs.tf&lt;/code&gt; file with the following content, and run &lt;code&gt;terraform apply&lt;/code&gt;. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sqs_queue"&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-queue"&lt;/span&gt;
  &lt;span class="nx"&gt;receive_wait_time_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"prod_sqs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-sqs-user"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user_policy"&lt;/span&gt; &lt;span class="s2"&gt;"prod_sqs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"sqs:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:sqs:*:*:*"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_access_key"&lt;/span&gt; &lt;span class="s2"&gt;"prod_sqs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here we've created a new &lt;a href="https://aws.amazon.com/sqs/" rel="noopener noreferrer"&gt;SQS&lt;/a&gt; instance, a new &lt;a href="https://aws.amazon.com/iam/" rel="noopener noreferrer"&gt;IAM&lt;/a&gt; user, granted access to this SQS instance to this user, and created an &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html" rel="noopener noreferrer"&gt;IAM Access Key&lt;/a&gt; to give access to SQS from Django application. Let's look at the new instance in AWS console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Faws_sqs_console_new_instance.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Faws_sqs_console_new_instance.png" alt="A new instance in AWS SQS console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Activating a region
&lt;/h3&gt;

&lt;p&gt;Now, let's create an ECS for the Celery worker and beat. &lt;/p&gt;

&lt;p&gt;First, let's "activate" our region on AWS. For some reason, AWS doesn't allow you to create more than 2 ECS containers in the region. You need to create an EC2 instance in this region to remove this limit. Let's do it manually in &lt;a href="https://us-east-2.console.aws.amazon.com/ec2/v2/home" rel="noopener noreferrer"&gt;EC2 Console&lt;/a&gt;. Be sure that you use your AWS region.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click "Launch Instance"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_launch_instance.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_launch_instance.png" alt="Clicking Launch Instance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick any name for your instance. I'll go with &lt;code&gt;Test Server&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_pick_name.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_pick_name.png" alt="Picking name for EC2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scroll down to the "Key pair" card and pick "Proceed without a key pair". We won't connect to this server, so we don't need one. Then click "Launch Instance" to create a new instance in your region.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_launch_final.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_launch_final.png" alt="EC2 Launch final"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS will soon create an instance. Go to the &lt;a href="https://us-east-2.console.aws.amazon.com/ec2/v2/home?region=us-east-2#Instances:" rel="noopener noreferrer"&gt;Instances&lt;/a&gt; tab in EC2 Console and verify that you have 1 "Running" instance. &lt;/p&gt;

&lt;p&gt;After that, you can terminate the instance because we don't need it. Pick the instance, click the "Instance state", then "Terminate instance", and confirm termination. AWS will permanently remove your EC2 instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_terminate_instance.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fec2_terminate_instance.png" alt="Terminate EC2 instance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, now we can create more than two ECS containers. Let's continue creating Celery ECS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running a Celery via ECS
&lt;/h3&gt;

&lt;p&gt;Now, let's define our Celery ECS service. First, add new variables in &lt;code&gt;ecs.tf&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;container_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;

    &lt;span class="nx"&gt;sqs_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_access_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;sqs_secret_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_access_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;
    &lt;span class="nx"&gt;sqs_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and pass it to containers in &lt;code&gt;backend_container.json.tpl&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_REGION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${region}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CELERY_BROKER_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs://${urlencode(sqs_access_key)}:${urlencode(sqs_secret_key)}@"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CELERY_TASK_DEFAULT_QUEUE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${sqs_name}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So, we passed SQS credentials to ECS services. Then, add the following content in &lt;code&gt;ecs.tf&lt;/code&gt; and run &lt;code&gt;terraform apply&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="err"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Cloudwatch Logs&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_stream"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_worker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-worker"&lt;/span&gt;
  &lt;span class="nx"&gt;log_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_stream"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_beat"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-worker"&lt;/span&gt;
  &lt;span class="nx"&gt;log_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Worker&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_worker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_mode&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;
  &lt;span class="nx"&gt;requires_compatibilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;cpu&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;

  &lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend-worker"&lt;/span&gt;
  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"templates/backend_container.json.tpl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-worker"&lt;/span&gt;
        &lt;span class="nx"&gt;command&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"celery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"django_aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--loglevel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;log_stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;task_role_arn&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_service"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_worker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-worker"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt;                            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_definition&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_task_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;desired_count&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_minimum_healthy_percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_maximum_percent&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
  &lt;span class="nx"&gt;launch_type&lt;/span&gt;                        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;
  &lt;span class="nx"&gt;scheduling_strategy&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"REPLICA"&lt;/span&gt;
  &lt;span class="nx"&gt;enable_execute_command&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;network_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_ecs_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;subnets&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_private_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_private_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;assign_public_ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Beat&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_beat"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_mode&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;
  &lt;span class="nx"&gt;requires_compatibilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;cpu&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;

  &lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend-beat"&lt;/span&gt;
  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"templates/backend_container.json.tpl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-beat"&lt;/span&gt;
        &lt;span class="nx"&gt;command&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"celery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"django_aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"beat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--loglevel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;log_stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;task_role_arn&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_service"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_beat"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-beat"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt;                            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_definition&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_task_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;desired_count&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_minimum_healthy_percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_maximum_percent&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
  &lt;span class="nx"&gt;launch_type&lt;/span&gt;                        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;
  &lt;span class="nx"&gt;scheduling_strategy&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"REPLICA"&lt;/span&gt;
  &lt;span class="nx"&gt;enable_execute_command&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;network_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_ecs_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;subnets&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_private_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_private_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;assign_public_ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here we created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/Working-with-log-groups-and-streams.html" rel="noopener noreferrer"&gt;Cloudwatch Logs streams&lt;/a&gt; for worker and beat. &lt;/li&gt;
&lt;li&gt;Worker &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html" rel="noopener noreferrer"&gt;ECS task definition&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html" rel="noopener noreferrer"&gt;ECS service&lt;/a&gt;. We specified &lt;code&gt;desired_count=2&lt;/code&gt; to show how multiple workers can run for the same queue. In the future we will scale worker ECS depending on CPU load.
&lt;/li&gt;
&lt;li&gt;Beat ECS task definition and ECS service. Here we specified &lt;code&gt;desired_count=1&lt;/code&gt; because we don't want to schedule duplicates for periodic tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's check our services in the &lt;a href="https://us-east-2.console.aws.amazon.com/ecs/home" rel="noopener noreferrer"&gt;ECS console&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Here are our worker and beat service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_services.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_services.png" alt="ECS worker and beat services"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are worker and beat tasks. You can see that ECS creates two tasks for the worker service and only one task for the beat service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_tasks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_tasks.png" alt="ECS worker tasks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are worker logs. For now, we see only beat tasks in logs:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_tasks_logs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_tasks_logs.png" alt="ECS worker logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's add a new task from the web. Hit &lt;a href="https://api.example53.xyz/create-task" rel="noopener noreferrer"&gt;https://api.example53.xyz/create-task&lt;/a&gt; URL (replace a domain with your one). You should see a 'Task added' message in response. Then, return to ECS worker logs, and pick the '30s' interval to see the most recent log events. You should see 'Starting web task' and 'Done web task' messages in the logs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_web_task_logs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_web_task_logs.png" alt="Web task logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we successfully run ECS for worker and beat processes and ensure that both web and beat Celery tasks are executed successfully. &lt;/p&gt;

&lt;p&gt;We are done with the &lt;code&gt;infrastructure&lt;/code&gt; repo so that you can commit and push the changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating deploy
&lt;/h3&gt;

&lt;p&gt;There is still one more task. To ensure that we will update our ECS services with every deployment, we need to modify our &lt;code&gt;./scripts/deploy.sh&lt;/code&gt;. Let's add the same instruction as for the &lt;code&gt;web&lt;/code&gt; service:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

...

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating web..."&lt;/span&gt;
aws ecs update-service &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--service&lt;/span&gt; prod-backend-web &lt;span class="nt"&gt;--force-new-deployment&lt;/span&gt;  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"service.serviceName"&lt;/span&gt;  &lt;span class="nt"&gt;--output&lt;/span&gt; json
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating worker..."&lt;/span&gt;
aws ecs update-service &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--service&lt;/span&gt; prod-backend-worker &lt;span class="nt"&gt;--force-new-deployment&lt;/span&gt;  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"service.serviceName"&lt;/span&gt;  &lt;span class="nt"&gt;--output&lt;/span&gt; json
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating beat..."&lt;/span&gt;
aws ecs update-service &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--service&lt;/span&gt; prod-backend-beat &lt;span class="nt"&gt;--force-new-deployment&lt;/span&gt;  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"service.serviceName"&lt;/span&gt;  &lt;span class="nt"&gt;--output&lt;/span&gt; json

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done!"&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So, we will force a new deployment for the worker and beat services on ECS with every push.&lt;/p&gt;

&lt;p&gt;Commit and push changes. Wait for CI/CD and check your services in ECS Console. After some time, new tasks will arise:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_new_deployment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fpart5%2Fecs_new_deployment.png" alt="ECS new deployment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can schedule more web tasks and see them in logs to ensure that things work as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The end
&lt;/h2&gt;

&lt;p&gt;Congratulations! We've successfully created an AWS SQS instance and added Celery worker + beat services to ECS. Our Django application can run long-living tasks in the background worker process.&lt;/p&gt;

&lt;p&gt;You can find the source code of backend and infrastructure projects &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need technical consulting on your project, check out our &lt;a href="https://daiquiri.team/services/technical-consulting?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_aws_5" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>django</category>
      <category>aws</category>
      <category>terraform</category>
      <category>celery</category>
    </item>
    <item>
      <title>Deploying Django Application on AWS with Terraform. Namecheap Domain + SSL</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Tue, 09 Aug 2022 11:01:00 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-namecheap-domain-ssl-19a5</link>
      <guid>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-namecheap-domain-ssl-19a5</guid>
      <description>&lt;p&gt;In previous steps, we've &lt;a href="//..."&gt;deployed Django with AWS ECS&lt;/a&gt;, &lt;a href="//..."&gt;connected it to the PostgreSQL RDS&lt;/a&gt;, and &lt;a href="//..."&gt;set up GitLab CI/CD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this step, we are going to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect &lt;a href="https://www.namecheap.com/" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; domain to &lt;a href="https://aws.amazon.com/route53/" rel="noopener noreferrer"&gt;Route53 DNS&lt;/a&gt; zone.&lt;/li&gt;
&lt;li&gt;Create an SSL certificate with &lt;a href="https://aws.amazon.com/certificate-manager/" rel="noopener noreferrer"&gt;Certificate Manager&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Reroute HTTP traffic to HTTPS and disable ALB host for Django application.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;/health/&lt;/code&gt; route for Health Checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Setting up Namecheap API
&lt;/h3&gt;

&lt;p&gt;I already have a domain name on &lt;a href="https://www.namecheap.com/" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt;. So I choose to connect the Namecheap domain to an AWS Route53 zone. But you can &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register.html" rel="noopener noreferrer"&gt;register domain&lt;/a&gt; with AWS Route53.&lt;/p&gt;

&lt;p&gt;First, let's enable API access for Namecheap. Look through &lt;a href="https://www.namecheap.com/support/api/intro/" rel="noopener noreferrer"&gt;this&lt;/a&gt; guide to receive &lt;code&gt;APIKey&lt;/code&gt; and add your IP to the whitelist.&lt;/p&gt;

&lt;p&gt;Second, add the &lt;a href="https://registry.terraform.io/providers/namecheap/namecheap/latest/docs" rel="noopener noreferrer"&gt;Namecheap provider&lt;/a&gt; to Terraform project. Add to the &lt;code&gt;provider.tf&lt;/code&gt; file following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;namecheap&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"namecheap/namecheap"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 2.0.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"namecheap"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namecheap_api_username&lt;/span&gt;
  &lt;span class="nx"&gt;api_user&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namecheap_api_username&lt;/span&gt;
  &lt;span class="nx"&gt;api_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namecheap_api_key&lt;/span&gt;
  &lt;span class="nx"&gt;use_sandbox&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;variables.tf&lt;/code&gt; add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Namecheap&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"namecheap_api_username"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Namecheap APIUsername"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"namecheap_api_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Namecheap APIKey"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also add &lt;code&gt;TF_VAR_namecheap_api_username&lt;/code&gt; and &lt;code&gt;TF_VAR_namecheap_api_key&lt;/code&gt; variables to &lt;code&gt;.env&lt;/code&gt; to provide values to the corresponding Terraform variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TF_VAR_namecheap_api_username=YOUR_API_USERNAME
TF_VAR_namecheap_api_key=YOUR_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import &lt;code&gt;.env&lt;/code&gt; variables with &lt;code&gt;export $(cat .env | xargs)&lt;/code&gt; and run &lt;code&gt;terraform init&lt;/code&gt; to add a Namecheap provider to the project. &lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting Domain to AWS
&lt;/h3&gt;

&lt;p&gt;Now, let's create a Route53 zone for the Namecheap domain and set up AWS nameservers. Thus, all DNS queries will be routed to the AWS Route53 nameservers, and we can manage DNS records from the AWS Route53 zone. &lt;/p&gt;

&lt;p&gt;Add to the &lt;code&gt;variables.tf&lt;/code&gt; following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Domains&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_base_domain"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Base domain for production"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example53.xyz"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_domain"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Backend web domain for production"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example53.xyz"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;route53.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_zone"&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_base_domain&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"namecheap_domain_records"&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_base_domain&lt;/span&gt;
  &lt;span class="nx"&gt;mode&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"OVERWRITE"&lt;/span&gt;

  &lt;span class="nx"&gt;nameservers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name_servers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name_servers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name_servers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name_servers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;terraform apply&lt;/code&gt;. Check nameservers on Namecheap:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fnamecheap_nameservers.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fnamecheap_nameservers.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating SSL Certificate
&lt;/h3&gt;

&lt;p&gt;Now, let's create an SSL certificate and set up DNS A record for &lt;code&gt;api.example53.xyz&lt;/code&gt; domain.&lt;/p&gt;

&lt;p&gt;Add to the &lt;code&gt;route53.tf&lt;/code&gt; following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="err"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_acm_certificate"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;domain_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_domain&lt;/span&gt;
  &lt;span class="nx"&gt;validation_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DNS"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_certificate_validation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;dvo&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_acm_certificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain_validation_options&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dvo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dvo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_record_name&lt;/span&gt;
      &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dvo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_record_value&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dvo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_record_type&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;allow_overwrite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;ttl&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_acm_certificate_validation"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;certificate_arn&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_acm_certificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;validation_record_fqdns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_certificate_validation&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fqdn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_a"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_domain&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are going to create a new &lt;a href="https://aws.amazon.com/certificate-manager/" rel="noopener noreferrer"&gt;SSL certificate&lt;/a&gt; for &lt;code&gt;api.example53.xyz&lt;/code&gt;, validate the SSL certificate via &lt;a href="https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html" rel="noopener noreferrer"&gt;DNS CNAME&lt;/a&gt; record, and &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-elb-load-balancer.html" rel="noopener noreferrer"&gt;add DNS A record to Load Balancer&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Apply changes with &lt;code&gt;terraform apply&lt;/code&gt; and wait for certificate validation. Usually, it takes up to several minutes. But in some cases, it can take several hours. You can check more info &lt;a href="https://aws.amazon.com/blogs/security/easier-certificate-validation-using-dns-with-aws-certificate-manager/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Redirecting HTTP to HTTPS
&lt;/h3&gt;

&lt;p&gt;Now let's use the issued SSL certificate to enable HTTPS. Replace the &lt;code&gt;resource "aws_lb_listener" "prod_http"&lt;/code&gt; block in the &lt;code&gt;load_balancer.tf&lt;/code&gt; with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Target listener for http:80&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_listener"&lt;/span&gt; &lt;span class="s2"&gt;"prod_http"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"80"&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect"&lt;/span&gt;
    &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;port&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"443"&lt;/span&gt;
      &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTPS"&lt;/span&gt;
      &lt;span class="nx"&gt;status_code&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP_301"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Target listener for https:443&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_alb_listener"&lt;/span&gt; &lt;span class="s2"&gt;"prod_https"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"443"&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTPS"&lt;/span&gt;
  &lt;span class="nx"&gt;ssl_policy&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ELBSecurityPolicy-2016-08"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"forward"&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;certificate_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_acm_certificate_validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certificate_arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we redirect unsecured HTTP traffic to HTTPS and add a listener for the HTTPS port. Apply changes and check &lt;a href="https://api.example53.xyz" rel="noopener noreferrer"&gt;https://api.example53.xyz&lt;/a&gt; URL. You should see Django starting page. &lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the ALLOWED_HOSTS variable
&lt;/h3&gt;

&lt;p&gt;Now, let's provide the &lt;a href="https://docs.djangoproject.com/en/3.2/ref/settings/#std-setting-ALLOWED_HOSTS" rel="noopener noreferrer"&gt;&lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;&lt;/a&gt; setting to the Django app. It's important to prevent &lt;a href="https://docs.djangoproject.com/en/3.2/topics/security/#host-headers-virtual-hosting" rel="noopener noreferrer"&gt;HTTP Host header attacks&lt;/a&gt;. So, Django Application should only accept our domain &lt;code&gt;api.example53.xyz&lt;/code&gt; in the host header. &lt;/p&gt;

&lt;p&gt;Now Django accepts any domain, for example, Load Balancer's domain. Visit &lt;code&gt;https://prod-1222631842.us-east-2.elb.amazonaws.com&lt;/code&gt; to check this fact. You can ignore the warning about an invalid SSL Certificate and see that Django responds to this host. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_invalid_domain.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_invalid_domain.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, let's disable a Debug mode and remove the &lt;code&gt;SECRET_KEY&lt;/code&gt; value from the code to improve security. Add the &lt;code&gt;TF_VAR_prod_backend_secret_key&lt;/code&gt; variable with a random generated value to the &lt;code&gt;.env&lt;/code&gt;, run &lt;code&gt;export $(cat .env | xargs)&lt;/code&gt;, and specify this var in &lt;code&gt;variables.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_secret_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production Django's SECRET_KEY"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, pass the domain name and &lt;code&gt;SECRET_KEY&lt;/code&gt; in &lt;code&gt;ecs.tf&lt;/code&gt;, set up &lt;code&gt;SECRET_KEY&lt;/code&gt;, &lt;code&gt;DEBUG&lt;/code&gt;, and &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; variables in &lt;code&gt;backend_container.json.tpl&lt;/code&gt; and apply changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;container_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;

    &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_domain&lt;/span&gt;
    &lt;span class="nx"&gt;secret_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_secret_key&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SECRET_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${secret_key}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEBUG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALLOWED_HOSTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${domain}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have all necessary environment variables on ECS. Move to the Django app and change &lt;code&gt;settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SECURITY WARNING: keep the secret key used in production secret!
&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ewfi83f2ofee3398fh2ofno24f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# SECURITY WARNING: don't run with debug turned on in production!
&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we receive &lt;code&gt;SECRET_KEY&lt;/code&gt;, &lt;code&gt;DEBUG&lt;/code&gt;, and &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; variables from env variables. We provide default &lt;code&gt;SECRET_KEY&lt;/code&gt; to allow running the application locally without specifying &lt;code&gt;SECRET_KEY&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; file. &lt;/p&gt;

&lt;h3&gt;
  
  
  Health Check
&lt;/h3&gt;

&lt;p&gt;All user's requests would have Host header &lt;code&gt;api.example53.xyz&lt;/code&gt;. But, we also have &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-health-checks.html" rel="noopener noreferrer"&gt;health check requests&lt;/a&gt; from a load balancer. &lt;/p&gt;

&lt;p&gt;AWS load balancers can automatically check our container's health. If the container responds correctly, the load balancer considers that target is &lt;em&gt;healthy&lt;/em&gt;. Otherwise, the target will be marked as &lt;em&gt;unhealthy&lt;/em&gt;. Load balancer routes traffic to &lt;em&gt;healthy&lt;/em&gt; targets only. Thus, user requests wouldn't hit &lt;em&gt;unhealthy&lt;/em&gt; containers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For HTTP or HTTPS health check requests, the host header contains the IP address of the load balancer node and the listener port, not the IP address of the target and the health check port.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We don't know the Load Balancer IP address. Also, this IP can be changed after some time. Therefore, we cannot add the Load Balancer host to the &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The solution is to write a custom &lt;a href="https://docs.djangoproject.com/en/3.2/topics/http/middleware/" rel="noopener noreferrer"&gt;middleware&lt;/a&gt; that returns a successful response before the host checking in the &lt;code&gt;SecurityMiddleware&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, go to the infrastructure, change the health check URL in &lt;code&gt;load_balancer.tf&lt;/code&gt; to &lt;code&gt;/health/&lt;/code&gt;, and apply changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/health/"&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return to the Django project and create &lt;code&gt;django_aws/middleware.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health_check_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Health-check request
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Check DB connection is healthy
&lt;/span&gt;            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Healthy!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Regular requests
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;middleware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this middleware to the &lt;code&gt;settings.py&lt;/code&gt; before the &lt;code&gt;SecurityMiddleware&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django_aws.middleware.health_check_middleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.middleware.security.SecurityMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python manage.py runserver&lt;/code&gt; and check &lt;code&gt;127.0.0.1:8000/health/&lt;/code&gt; URL in your browser. You should see the text response &lt;code&gt;Healthy!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Commit and push changes, wait for the pipeline and check the Load Balancer domain again &lt;code&gt;https://prod-57218461274.us-east-2.elb.amazonaws.com/&lt;/code&gt;. Now, we get a &lt;code&gt;Bad Request&lt;/code&gt; error. Also, we didn't see a traceback or other debug information, so we can be sure that the debug mode is disabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fwrong_domain_400.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fwrong_domain_400.png" alt="400 Bad Request"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, navigate to &lt;code&gt;https://prod-57218461274.us-east-2.elb.amazonaws.com/health/&lt;/code&gt; in your browser to check &lt;code&gt;health_check_middleware&lt;/code&gt;. We get the &lt;code&gt;Healthy!&lt;/code&gt; response. So, the  Load Balancer will be able to check containers' health without providing the correct Host header.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fwrong_domain_healthcheck.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fwrong_domain_healthcheck.png" alt="Health Check Success Response"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! We've successfully set up a domain name, created health checks, disabled the debug mode, and removed &lt;code&gt;SECRET_KEY&lt;/code&gt; value from the source code. Do not forget to push infrastructure code to GitLab.&lt;/p&gt;

&lt;p&gt;You can find the source code of backend and infrastructure projects &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need technical consulting on your project, check out our &lt;a href="https://daiquiri.team/services/technical-consulting?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_aws_4" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>django</category>
      <category>terraform</category>
      <category>aws</category>
      <category>namecheap</category>
    </item>
    <item>
      <title>Deploying Django Application on AWS with Terraform. GitLab CI/CD</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Wed, 03 Aug 2022 07:07:00 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-gitlab-cicd-13am</link>
      <guid>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-gitlab-cicd-13am</guid>
      <description>&lt;p&gt;In previous parts, we've &lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-minimal-working-setup-587g"&gt;deployed the Django web application to ECS&lt;/a&gt; and &lt;a href="//..."&gt;connected PostgreSQL&lt;/a&gt; to it. But now, we have to deploy application changes manually. In this part, we are going to automate this process with the following steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a GitLab group and projects for the &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;backend&lt;/a&gt; and the &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;infrastructure&lt;/a&gt; repositories.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;test&lt;/code&gt; CI/CD stage to run tests.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;build&lt;/code&gt; CI/CD stage to build a docker image and push it to the ECR.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;deploy&lt;/code&gt; CI/CD stage to update the application on AWS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creating Projects
&lt;/h3&gt;

&lt;p&gt;Let's start with creating a GitLab group and projects. Go to &lt;a href="https://gitlab.com/" rel="noopener noreferrer"&gt;gitlab.com&lt;/a&gt; and register an account. &lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://gitlab.com/groups/new#create-group-pane" rel="noopener noreferrer"&gt;create&lt;/a&gt; a &lt;a href="https://docs.gitlab.com/ee/user/group/" rel="noopener noreferrer"&gt;GitLab group&lt;/a&gt;. A group is like a folder for projects. You can configure shared settings like access policy and CI variables for them.&lt;/p&gt;

&lt;p&gt;Create projects for &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;Django&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; in this group. Be sure to remove the "Initialize repository with a README" option to create a clean repository.&lt;/p&gt;

&lt;p&gt;Also, add your ssh key to &lt;a href="https://gitlab.com/-/profile/keys" rel="noopener noreferrer"&gt;SSH Keys&lt;/a&gt; section. It allows you to access your projects via Git.&lt;/p&gt;

&lt;p&gt;Now, let's push both of our repositories to GitLab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Push backend&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../django-aws-backend
git remote add origin git@gitlab.com:django-aws/django-aws-backend 
git push &lt;span class="nt"&gt;--set-upstream&lt;/span&gt; origin main

&lt;span class="c"&gt;# Push infrastructure&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../django-aws-infrastructure
git remote add origin git@gitlab.com:django-aws/django-aws-infrastructure 
git push &lt;span class="nt"&gt;--set-upstream&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your GitLab projects in a browser and verify that the push is successful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage: Test
&lt;/h3&gt;

&lt;p&gt;Now let's add unit tests check to the Django project. Django's unit tests use a Python standard library module &lt;a href="https://docs.python.org/3/library/unittest.html#module-unittest" rel="noopener noreferrer"&gt;unittest&lt;/a&gt;. See more info about testing a Django application &lt;a href="https://docs.djangoproject.com/en/3.2/topics/testing/overview/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Go to the Django project, activate &lt;code&gt;venv&lt;/code&gt; and start a PostgreSQL Docker container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../django-aws-backend
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./venv/bin/activate
&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's create &lt;code&gt;django_aws/tests.py&lt;/code&gt; and add a simple test to check DB connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.test&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DbConnectionTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_db_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_usable&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python manage.py test&lt;/code&gt; locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py &lt;span class="nb"&gt;test
&lt;/span&gt;Creating &lt;span class="nb"&gt;test &lt;/span&gt;database &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;alias&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;...
System check identified no issues &lt;span class="o"&gt;(&lt;/span&gt;0 silenced&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nt"&gt;----------------------------------------------------------------------&lt;/span&gt;
Ran 1 &lt;span class="nb"&gt;test &lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;0.010s

OK
Destroying &lt;span class="nb"&gt;test &lt;/span&gt;database &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;alias&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's add this check on GitLab's side. Take a look &lt;a href="https://docs.gitlab.com/ee/ci/quick_start/" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you have no idea what GitLab CI is. Generally, GitLab will run code specified in &lt;a href="https://docs.gitlab.com/ee/ci/yaml/" rel="noopener noreferrer"&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;&lt;/a&gt; on every push in the repository.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file with following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.10&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
  &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;django_aws&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:password@postgres:5432/django_aws&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres:14.2&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;requirements.txt&lt;/span&gt;
      &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${CI_JOB_NAME}&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;venv&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.cache/pip&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python -m venv venv&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;. venv/bin/activate&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install --upgrade pip&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python manage.py test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: python:3.10&lt;/code&gt;. By default, GitLab runs CI/CD pipeline on shared runners hosted by GitLab using the &lt;a href="https://docs.gitlab.com/runner/executors/docker.html" rel="noopener noreferrer"&gt;docker executor&lt;/a&gt;. Here we specify docker image for the executor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stages:&lt;/code&gt;. A pipeline can have several stages. Now we have only the &lt;code&gt;test&lt;/code&gt; stage. In the future, we will have three stages: &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, and &lt;code&gt;deploy&lt;/code&gt;. GitLab will execute stages in the specified order. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;services: postgres:14.2&lt;/code&gt;. GitLab will run PostgreSQL in a separate container during the &lt;code&gt;test&lt;/code&gt; stage. So, our Django application will be able to run DB-related tests. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variables: POSTGRES_PASSWORD, POSTGRES_DB DATABASE_URL&lt;/code&gt; - environment variables for both Django and PostgreSQL docker containers.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache: key:&lt;/code&gt;. Here we cache the result of the &lt;code&gt;pip install&lt;/code&gt; command to speed up pipeline executions. If the  key &lt;code&gt;files: requirements.txt&lt;/code&gt; hasn't changed since the last run GitLab will download the cache for &lt;code&gt;venv&lt;/code&gt; and &lt;code&gt;.cache/pip&lt;/code&gt; directories.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script: ...&lt;/code&gt; is commands to execute. GitLab executes commands one by one. If some command returns a non-zero exit code, GitLab will interrupt pipeline execution and mark it &lt;em&gt;Failed&lt;/em&gt;. If all commands have been executed successfully, GitLab marks the current stage as &lt;em&gt;Successful&lt;/em&gt; and goes to the next stage. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, push the changes and take a look at &lt;a href="https://gitlab.com/django-aws/django-aws-backend/-/pipelines" rel="noopener noreferrer"&gt;CI/CD&lt;/a&gt; tab of your GitLab Django project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add gitlab-ci; add test"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_test.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_test.png" alt="Tests is running"&gt;&lt;/a&gt; &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_test_passed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_test_passed.png" alt="Tests have been passed"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Stage: Build
&lt;/h3&gt;

&lt;p&gt;Tests are passed, so we move on to the &lt;code&gt;build&lt;/code&gt; stage. At this stage, we need to connect our GitLab account with our AWS account to grant GitLab access to the ECR repository. For this, we'll create a separate AWS user &lt;code&gt;gitlab&lt;/code&gt; with limited permissions. Let's go to the AWS IAM console and create a &lt;a href="https://us-east-1.console.aws.amazon.com/iam/home#/users$new?step=details" rel="noopener noreferrer"&gt;new user&lt;/a&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can find instructions on how to create an AWS user &lt;a href="https://dev.toTODO:_Link_To_the_first_chapter"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Add to this user &lt;code&gt;AmazonEC2ContainerRegistryPowerUser&lt;/code&gt; policy to enable read and write permission to any ECR repository on this account. Proceed to the final step of user creation and save &lt;code&gt;ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;SECRET_ACCESS_KEY&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Now go to the GitLab group settings and add &lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, and &lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt; variables. GitLab runner will use these credentials for AWS CLI calls. &lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/rUv3P8NRAx4"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Make sure to mask sensitive variables &lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, and &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; to hide their values in GitLab logs. Take a look at the &lt;a href="https://docs.gitlab.com/ee/ci/cloud_deployment/index.html#aws" rel="noopener noreferrer"&gt;AWS deployment&lt;/a&gt; documentation page for more information.&lt;/p&gt;

&lt;p&gt;Then add to the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; build block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;...&lt;/span&gt;
  &lt;span class="s"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp://docker:2375&lt;/span&gt;
  &lt;span class="s"&gt;DOCKER_TLS_CERTDIR&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;AWS_REGISTRY_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${CI_PROJECT_NAME}:latest"&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker:20.10-dind&lt;/span&gt;
  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;aws ecr get-login-password | docker login --username AWS --password-stdin $AWS_REGISTRY_URL&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker pull $AWS_REGISTRY_URL || &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build --cache-from $AWS_REGISTRY_URL -t $AWS_REGISTRY_URL .&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push $AWS_REGISTRY_URL&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest&lt;/code&gt;. This image allows using AWS CLI commands during CI/CD.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;services: docker:20.10-dind&lt;/code&gt;. We use &lt;a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker" rel="noopener noreferrer"&gt;docker-in-docker&lt;/a&gt; to build the docker image inside the &lt;a href="https://docs.gitlab.com/runner/executors/docker.html" rel="noopener noreferrer"&gt;docker executor&lt;/a&gt;. Docker starts as a separate service, so we can access the docker daemon from the GitLab job.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variables: DOCKER_HOST, DOCKER_TLS_CERTDIR&lt;/code&gt;. Specify the path to the docker-in-docker daemon and disable TLS connection.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_REGISTRY_URL&lt;/code&gt;. Construct the ECR URL from the project name. Ensure that the names of the ECR repo and Gitlab project are the same.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;before_script: aws ecr get-login-password&lt;/code&gt;. Log in to ECR.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker pull $AWS_REGISTRY_URL || true&lt;/code&gt;. Pull actual image to use it as cache&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker push $AWS_REGISTRY_URL&lt;/code&gt;. Upload image to ECR.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;only: main&lt;/code&gt;. We'll build a docker image for &lt;code&gt;main&lt;/code&gt; branch only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commit your changes and check a CI/CD pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add build stage"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_build_passed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_build_passed.png" alt="Build passed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage: Deploy
&lt;/h3&gt;

&lt;p&gt;Build stage passed, now we'll deploy container from ECR to ECS. &lt;/p&gt;

&lt;p&gt;But let's start with running migrations. We want to run migrations as separate ECS task to avoid any side effects on web application. Go to the &lt;code&gt;infrastructure&lt;/code&gt; project and make following changes in &lt;code&gt;ecs.tf&lt;/code&gt; and apply changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Production cluster&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;container_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;

    &lt;span class="nx"&gt;image&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_url&lt;/span&gt;
    &lt;span class="nx"&gt;log_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

    &lt;span class="nx"&gt;rds_db_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_db_name&lt;/span&gt;
    &lt;span class="nx"&gt;rds_username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_username&lt;/span&gt;
    &lt;span class="nx"&gt;rds_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_password&lt;/span&gt;
    &lt;span class="nx"&gt;rds_hostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Backend web task definition and service&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"templates/backend_container.json.tpl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-web"&lt;/span&gt;
        &lt;span class="nx"&gt;command&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"gunicorn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;":8000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"django_aws.wsgi:application"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;log_stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;# Cloudwatch Logs&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_stream"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_migrations"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-migrations"&lt;/span&gt;
  &lt;span class="nx"&gt;log_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Migrations&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_migration"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_mode&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;
  &lt;span class="nx"&gt;requires_compatibilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;cpu&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;

  &lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend-migration"&lt;/span&gt;
  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"templates/backend_container.json.tpl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-migration"&lt;/span&gt;
        &lt;span class="nx"&gt;command&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"manage.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"migrate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;log_stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_migrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;task_role_arn&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_backend_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we've moved the same variables for &lt;code&gt;migration&lt;/code&gt; and &lt;code&gt;web&lt;/code&gt; containers in &lt;code&gt;container_vars&lt;/code&gt;. Then we've created a separate task definition and log stream for the migration container. Now we can the start task with a specified task definition to apply migrations for every release. &lt;/p&gt;

&lt;p&gt;Now let's create a deployment script. Return the Django project and create &lt;code&gt;scripts/deploy.sh&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../django-aws-backend
&lt;span class="nb"&gt;mkdir &lt;/span&gt;scripts 
&lt;span class="nb"&gt;touch &lt;/span&gt;scripts/deploy.sh 
&lt;span class="nb"&gt;chmod &lt;/span&gt;777 scripts/deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Collect ECS_GROUP_ID and PRIVATE_SUBNET_ID for running migrations&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Collecting data..."&lt;/span&gt;
&lt;span class="nv"&gt;ECS_GROUP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-security-groups &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;group-name,Values&lt;span class="o"&gt;=&lt;/span&gt;prod-ecs-backend &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"SecurityGroups[*][GroupId]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PRIVATE_SUBNET_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-subnets  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=prod-private-1"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Subnets[*][SubnetId]"&lt;/span&gt;  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running migration task..."&lt;/span&gt;
&lt;span class="c"&gt;# Construct NETWORK_CONFIGURATON to run migtaion task &lt;/span&gt;
&lt;span class="nv"&gt;NETWORK_CONFIGURATON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;awsvpcConfiguration&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;subnets&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: [&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PRIVATE_SUBNET_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;], &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;securityGroups&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: [&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ECS_GROUP_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;],&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;assignPublicIp&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;DISABLED&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}}"&lt;/span&gt;
&lt;span class="c"&gt;# Start migration task&lt;/span&gt;
&lt;span class="nv"&gt;MIGRATION_TASK_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs run-task &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--task-definition&lt;/span&gt; backend-migration &lt;span class="nt"&gt;--count&lt;/span&gt; 1 &lt;span class="nt"&gt;--launch-type&lt;/span&gt; FARGATE &lt;span class="nt"&gt;--network-configuration&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NETWORK_CONFIGURATON&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'tasks[*][taskArn]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Task &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_TASK_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; running..."&lt;/span&gt;
&lt;span class="c"&gt;# Wait migration task to complete&lt;/span&gt;
aws ecs &lt;span class="nb"&gt;wait &lt;/span&gt;tasks-stopped &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--tasks&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_TASK_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating web..."&lt;/span&gt;
&lt;span class="c"&gt;# Updating web service&lt;/span&gt;
aws ecs update-service &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--service&lt;/span&gt; prod-backend-web &lt;span class="nt"&gt;--force-new-deployment&lt;/span&gt;  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"service.serviceName"&lt;/span&gt;  &lt;span class="nt"&gt;--output&lt;/span&gt; json

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check the docs for &lt;a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecs/run-task.html" rel="noopener noreferrer"&gt;run-task&lt;/a&gt;, &lt;a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecs/wait/tasks-stopped.html" rel="noopener noreferrer"&gt;wait tasks-stopped&lt;/a&gt;, and &lt;a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecs/update-service.html" rel="noopener noreferrer"&gt;update-service&lt;/a&gt; commands.&lt;/p&gt;

&lt;p&gt;Run this script locally to check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./scripts/deploy.sh
Collecting data...
Running migration task...
Task arn:aws:ecs:us-east-2:947134793474:task/prod/dcc06d7ec1ac4bb69bba445565eddf8b running...
Updating web...
&lt;span class="s2"&gt;"prod-backend-web"&lt;/span&gt;
Done!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've successfully run this script by the &lt;code&gt;admin&lt;/code&gt; user. GitLab CI/CD will use the &lt;code&gt;gitlab&lt;/code&gt; user, so we need to grant all required permissions.&lt;/p&gt;

&lt;p&gt;Let's create a new policy &lt;code&gt;gitlab-deploy-ecs&lt;/code&gt;, and add it to the &lt;code&gt;gitlab&lt;/code&gt; user. Go to the &lt;a href="https://us-east-1.console.aws.amazon.com/iam/home" rel="noopener noreferrer"&gt;IAM Console&lt;/a&gt;, select the "Users" tab and click on the &lt;code&gt;gitlab&lt;/code&gt; user. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fiam_add_ecs_policy_1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fiam_add_ecs_policy_1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, click on "Add inline policy" and add the JSON policy definition. You need to use your &lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt; instead of &lt;code&gt;947134793474&lt;/code&gt; number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSubnets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSecurityGroups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecs:UpdateService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ecs:DescribeTasks"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:PassRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::947134793474:role/ecs-task-execution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::947134793474:role/prod-backend-task"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecs:RunTask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ecs:us-east-2:947134793474:task-definition/backend-migration*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fiam_add_ecs_policy_2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fiam_add_ecs_policy_2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explaining policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ec2:DescribeSubnets&lt;/code&gt;, &lt;code&gt;ec2:DescribeSecurityGroups&lt;/code&gt; — for "Collecting data..." stage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ecs:RunTask&lt;/code&gt;, &lt;code&gt;iam:PassRole&lt;/code&gt; — for running a migration ECS task.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ecs:DescribeTasks&lt;/code&gt; — for waiting a migration ECS task ends.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ecs:UpdateService&lt;/code&gt; — for updating the ECS Django web application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click "Review Policy", name the policy &lt;code&gt;gitlab-ecs-deploy&lt;/code&gt; and click "Create Policy". Now, the &lt;code&gt;gitlab&lt;/code&gt; user will be able to execute the &lt;code&gt;deploy.sh&lt;/code&gt; script.&lt;/p&gt;

&lt;p&gt;Add deploy stage to &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/deploy.sh&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the deploy stage, we simply run &lt;code&gt;deploy.sh&lt;/code&gt; script. We use the &lt;code&gt;aws-base&lt;/code&gt; image to have access to AWS CLI commands.&lt;/p&gt;

&lt;p&gt;Finally, let's add some changes to Django to see that our application will be updated automatically. Let's change the Django Admin header text. Add this line to &lt;code&gt;django_aws/urls.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Django AWS Admin Panel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now commit your changes and see how's your pipeline going.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add deploy stage"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_deploy_passed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fgitlab_ci_deploy_passed.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check your admin page &lt;code&gt;prod-57218461274.us-east-2.elb.amazonaws.com/admin&lt;/code&gt; and see a new title. It can take some time to update the ECS service. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fsuccessful_deployment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fsuccessful_deployment.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, do not forget to push infrastructure code to GitLab. &lt;/p&gt;

&lt;p&gt;Congratulations! We've successfully set up CI/CD with GitLab for our web Django web application. Now we can commit our code to the &lt;code&gt;main&lt;/code&gt; branch,  and GitLab CI/CD will automatically test, build and deploy it on the AWS.&lt;/p&gt;

&lt;p&gt;But the &lt;code&gt;prod-57218461274.us-east-2.elb.amazonaws.com&lt;/code&gt; domain looks not user-friendly :) Also, we need to secure the connection between a user and the Django application with an SSL certificate. In the next part, we'll connect the Namecheap domain to AWS and set up an SSL certificate for them.&lt;/p&gt;

&lt;p&gt;You can find the source code of backend and infrastructure projects &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need technical consulting on your project, check out our &lt;a href="https://daiquiri.team/services/technical-consulting?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_aws_3" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>gitlab</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Deploying Django Application on AWS with Terraform. Connecting PostgreSQL RDS</title>
      <dc:creator>Yevhen Bondar</dc:creator>
      <pubDate>Fri, 29 Jul 2022 09:50:00 +0000</pubDate>
      <link>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-postgresql-rds-2j0i</link>
      <guid>https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-connecting-postgresql-rds-2j0i</guid>
      <description>&lt;p&gt;In the previous &lt;a href="https://dev.to/daiquiri_team/deploying-django-application-on-aws-with-terraform-minimal-working-setup-587g"&gt;part of this guide&lt;/a&gt;, we deployed the Django web application on &lt;a href="https://aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;AWS ECS&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In this part, we'll create a &lt;a href="https://aws.amazon.com/rds/postgresql/" rel="noopener noreferrer"&gt;PostgreSQL RDS&lt;/a&gt; instance on AWS, connect it to the Django ECS task and enable access to &lt;a href="https://docs.djangoproject.com/en/3.2/ref/contrib/admin/" rel="noopener noreferrer"&gt;Django Admin&lt;/a&gt;. We choose PostgreSQL because it supports complex SQL queries, &lt;a href="https://www.postgresql.org/docs/current/mvcc.html" rel="noopener noreferrer"&gt;MVCC&lt;/a&gt;, good performance for concurrent queries, and has a large community. As AWS says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL has become the preferred open source relational database for many enterprise developers and start-ups, powering leading business and mobile applications. Amazon RDS makes it easy to set up, operate, and scale PostgreSQL deployments in the cloud. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Add PostgreSQL to Django project
&lt;/h3&gt;

&lt;p&gt;Go to the &lt;code&gt;django-aws-backend&lt;/code&gt; folder and activate virtual environment: &lt;code&gt;cd ../django-aws-backend &amp;amp;&amp;amp; . ./venv/bin/activate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, let's set up PostgreSQL via Docker for local development. Create &lt;code&gt;docker-compose.yml&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.1"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres:14"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5433:5432"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django_aws"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;docker-compose up -d&lt;/code&gt; to start a container. Now, let's connect Django to PostgreSQL. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We use a non-standard 5433 port to avoid potential conflict with your local PostgreSQL&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, we need to add pip packages: &lt;a href="https://pypi.org/project/psycopg2-binary/" rel="noopener noreferrer"&gt;psycopg2-binary&lt;/a&gt; for working with PostgreSQL and &lt;a href="https://django-environ.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;django-environ&lt;/a&gt; to retrieve PostgreSQL connection string from environment. Also, we'll add the &lt;a href="http://whitenoise.evans.io/en/stable/" rel="noopener noreferrer"&gt;WhiteNoise&lt;/a&gt; package to serve static files for Django Admin.  &lt;/p&gt;

&lt;p&gt;Add to the &lt;code&gt;requirements.txt&lt;/code&gt; file these packages and run &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;django-environ==0.8.1
psycopg2-binary==2.9.3
whitenoise==6.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, change &lt;code&gt;settings.py&lt;/code&gt;. First, let's load env variables. We try to read the &lt;code&gt;.env&lt;/code&gt; file in the project root if it exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;

&lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;

&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;env_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;env_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can use environment variables in &lt;code&gt;settings.py&lt;/code&gt;. Let's provide &lt;code&gt;DATABASES&lt;/code&gt; from env:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql://postgres:postgres@127.0.0.1:5433/django_aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've set a default value for the &lt;code&gt;DATABASE_URL&lt;/code&gt; to allow running the Django project locally without specifying this variable in the &lt;code&gt;.env&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;Third, add the &lt;code&gt;WhiteNoiseMiddleware&lt;/code&gt; to serve static files and specify the &lt;code&gt;STATIC_ROOT&lt;/code&gt; variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;django.middleware.security.SecurityMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whitenoise.middleware.WhiteNoiseMiddleware&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;STATIC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, add the &lt;code&gt;RUN ./manage.py collectstatic --noinput&lt;/code&gt; line to the bottom of &lt;code&gt;Dockerfile&lt;/code&gt; to collect static files in the static folder.&lt;/p&gt;

&lt;p&gt;Apply migrations, create a superuser and start a web server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;venv&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
...
&lt;span class="o"&gt;(&lt;/span&gt;venv&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py createsuperuser
Username: admin
Email address: 
Password: 
Password &lt;span class="o"&gt;(&lt;/span&gt;again&lt;span class="o"&gt;)&lt;/span&gt;: 
Superuser created 
&lt;span class="o"&gt;(&lt;/span&gt;venv&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$ &lt;/span&gt;python manage.py runserver
...
Starting development server at http://127.0.0.1:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to &lt;a href="http://127.0.0.1:8000/admin/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/admin/&lt;/a&gt; and sign in with your user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_admin_1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_admin_1.png" alt="Django Admin"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let's commit changes, build and push the docker image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt; 
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add postgresql, environ and static files serving"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 947134793474.dkr.ecr.us-east-2.amazonaws.com/django-aws-backend:latest
&lt;span class="nv"&gt;$ &lt;/span&gt;docker push 947134793474.dkr.ecr.us-east-2.amazonaws.com/django-aws-backend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are done with the Django part. Next, we create a PostgreSQL instance on AWS and connect it to the ECS task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating RDS
&lt;/h3&gt;

&lt;p&gt;AWS provides the &lt;a href="https://aws.amazon.com/rds/postgresql/" rel="noopener noreferrer"&gt;Relational Database Service&lt;/a&gt; to run a &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;. Now, we'll describe the RDS setup with Terraform. Go to the Terraform folder &lt;code&gt;../django-aws-infrastructure&lt;/code&gt; and add a &lt;code&gt;rds.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_subnet_group"&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_private_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_private_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
  &lt;span class="nx"&gt;db_name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_db_name&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_username&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_password&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5432"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"14.2"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_instance_class&lt;/span&gt;
  &lt;span class="nx"&gt;allocated_storage&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"20"&lt;/span&gt;
  &lt;span class="nx"&gt;storage_encrypted&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;db_subnet_group_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_subnet_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;multi_az&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;storage_type&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gp2"&lt;/span&gt;
  &lt;span class="nx"&gt;publicly_accessible&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;backup_retention_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="nx"&gt;skip_final_snapshot&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# RDS Security Group (traffic ECS -&amp;gt; RDS)&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"rds_prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rds-prod"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5432"&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5432"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_ecs_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we create an RDS instance, a DB subnet group, and a Security Group to allow incoming traffic from ECS 5432 port only. We'll specify &lt;code&gt;db_name&lt;/code&gt;, &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, and &lt;code&gt;instance_class&lt;/code&gt; in the &lt;code&gt;variables.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rds&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_rds_db_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS database name"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"django_aws"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_rds_username"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS database username"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"django_aws"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_rds_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres password for production DB"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"prod_rds_instance_class"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS instance type"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t4g.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we haven't provided a default value for the &lt;code&gt;prod_rds_password&lt;/code&gt; variable to prevent committing the database password to the repository. Terraform will ask you for a variable value if you try to apply these changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;terraform apply             
var.prod_rds_password
  postgres password &lt;span class="k"&gt;for &lt;/span&gt;production DB

  Enter a value: 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not convenient and error-prone to type a password every time. Gladly, Terraform can read variable values from the environment. Create &lt;code&gt;.env&lt;/code&gt; file with &lt;code&gt;TF_VAR_prod_rds_password=YOUR_PASSWORD&lt;/code&gt; variable. Interrupt password prompting, run &lt;code&gt;export $(cat .env | xargs)&lt;/code&gt; to load &lt;code&gt;.env&lt;/code&gt; variable, and rerun &lt;code&gt;terraform apply&lt;/code&gt;. Now Terraform picks the password from the &lt;code&gt;.env&lt;/code&gt; file and creates an RDS PostgreSQL instance. Check it in the &lt;a href="https://console.aws.amazon.com/rds#databases:" rel="noopener noreferrer"&gt;AWS RDS console&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Frds_instance.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Frds_instance.png" alt="RDS Instance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting to ECS
&lt;/h3&gt;

&lt;p&gt;Let's connect the RDS instance to ECS tasks.&lt;/p&gt;

&lt;p&gt;Add to &lt;code&gt;backend_container.json.tpl&lt;/code&gt; environment var &lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql://${rds_username}:${rds_password}@${rds_hostname}:5432/${rds_db_name}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add RDS variables to the &lt;code&gt;prod_backend_web&lt;/code&gt; task definition in the &lt;code&gt;ecs.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;templatefile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"templates/backend_container.json.tpl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;

      &lt;span class="nx"&gt;rds_db_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_db_name&lt;/span&gt;
      &lt;span class="nx"&gt;rds_username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_username&lt;/span&gt;
      &lt;span class="nx"&gt;rds_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod_rds_password&lt;/span&gt;
      &lt;span class="nx"&gt;rds_hostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's apply changes and update the ECS service with the new task definition. Run &lt;code&gt;terraform apply&lt;/code&gt;, stop current task via web console and wait for the new task to arise. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fecs_services_stop_tasks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fecs_services_stop_tasks.png" alt="Stopping ECS task"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, go to the admin URL on the load balancer hostname and try to log in with random credentials. You should get the &lt;code&gt;relation "auth_user" does not exist&lt;/code&gt; error. This error means that the Django application successfully connected to PostgreSQL, but no migrations was run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_no_migrations_error.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_no_migrations_error.png" alt="Django no migrations error"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Running migrations
&lt;/h3&gt;

&lt;p&gt;Now, we need to run migrations and create a superuser. But how can we do it? Our infrastructure has no EC2 instances to connect via SSH and run this command. The solution is &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html" rel="noopener noreferrer"&gt;ECS Exec&lt;/a&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With Amazon ECS Exec, you can directly interact with containers without needing to first interact with the host container operating system, open inbound ports, or manage SSH keys. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, we need to install the Session Manager plugin. For macOS, you can use &lt;code&gt;brew install session-manager-plugin&lt;/code&gt;. For other platforms, check this &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, we need to provide an IAM policy to the &lt;code&gt;prod_backend_task&lt;/code&gt; role and enable &lt;code&gt;execute_command&lt;/code&gt; for the ECS service. Add this code to the &lt;code&gt;ecs.tf&lt;/code&gt; and apply changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_service"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;enable_execute_command&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"prod_backend_task"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nx"&gt;inline_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-backend-task-ssmmessages"&lt;/span&gt;
    &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
      &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;"ssmmessages:CreateControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"ssmmessages:CreateDataChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"ssmmessages:OpenControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"ssmmessages:OpenDataChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
          &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop the old ECS task in web console and wait for the new task.&lt;/p&gt;

&lt;p&gt;Then let's retrieve the new &lt;code&gt;task_id&lt;/code&gt; via CLI and run &lt;code&gt;aws esc execute-command&lt;/code&gt;. Create a new file &lt;code&gt;touch backend_web_shell.sh &amp;amp;&amp;amp; chmod 777 backend_web_shell.sh&lt;/code&gt; with the content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs list-tasks &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--service-name&lt;/span&gt; prod-backend-web  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'taskArns[0]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{split($0,a,"/"); print a[3]}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
aws ecs execute-command &lt;span class="nt"&gt;--task&lt;/span&gt; &lt;span class="nv"&gt;$TASK_ID&lt;/span&gt; &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"bash"&lt;/span&gt; &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="nt"&gt;--cluster&lt;/span&gt; prod &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;./backend_web_shell.sh&lt;/code&gt; to shell into task. If you have some troubles, please check &lt;a href="https://github.com/aws-containers/amazon-ecs-exec-checker" rel="noopener noreferrer"&gt;Amazon ECS Exec Checker&lt;/a&gt;. This script checks that your CLI environment and ECS cluster/task are ready for ECS Exec.&lt;/p&gt;

&lt;p&gt;Run migrations and create a superuser from the task's console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./manage.py migrate
&lt;span class="nv"&gt;$ &lt;/span&gt;./manage.py createsuperuser 
Username: admin
Email address: 
Password: 
Password &lt;span class="o"&gt;(&lt;/span&gt;again&lt;span class="o"&gt;)&lt;/span&gt;: 
Superuser created 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, check your deployment admin URL and try to sign in with provided credentials. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_admin_2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3.eu-central-1.wasabisys.com%2Fdaiquiri-external-content%2Fblog%2Feugen1j%2Fdjango-aws%2Fimg%2Fdjango_admin_2.png" alt="Successful admin deployment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! We've successfully connected PostgreSQL to the ECS service. Now you can commit changes in the Terraform project and move to the next part. &lt;/p&gt;

&lt;p&gt;In the next part we'll set up &lt;a href="https://docs.gitlab.com/ee/ci/introduction/index.html#continuous-delivery" rel="noopener noreferrer"&gt;CI/CD&lt;/a&gt; with GitLab.&lt;/p&gt;

&lt;p&gt;You can find the source code of backend and infrastructure projects &lt;a href="https://gitlab.com/django-aws/django-aws-backend" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://gitlab.com/django-aws/django-aws-infrastructure" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need technical consulting on your project, check out our &lt;a href="https://daiquiri.team/services/technical-consulting?utm_medium=referral&amp;amp;utm_source=dev_to&amp;amp;utm_campaign=django_aws_2" rel="noopener noreferrer"&gt;website&lt;/a&gt; or connect with me directly on &lt;a href="https://www.linkedin.com/in/yevhen-bondar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>django</category>
      <category>postgres</category>
      <category>terraform</category>
    </item>
  </channel>
</rss>
